Merge "audio HAL: Support for external device connections"
diff --git a/audio/aidl/Android.bp b/audio/aidl/Android.bp
index 4927e83..0524110 100644
--- a/audio/aidl/Android.bp
+++ b/audio/aidl/Android.bp
@@ -75,6 +75,7 @@
         "android/hardware/audio/core/IModule.aidl",
         "android/hardware/audio/core/IStreamIn.aidl",
         "android/hardware/audio/core/IStreamOut.aidl",
+        "android/hardware/audio/core/ModuleDebug.aidl",
     ],
     imports: [
         "android.hardware.audio.common-V1",
@@ -86,6 +87,4 @@
             platform_apis: true,
         },
     },
-    versions: [
-    ],
 }
diff --git a/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl
index 33e8290..f8bc2c7 100644
--- a/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl
+++ b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl
@@ -34,11 +34,15 @@
 package android.hardware.audio.core;
 @VintfStability
 interface IModule {
+  void setModuleDebug(in android.hardware.audio.core.ModuleDebug debug);
+  android.media.audio.common.AudioPort connectExternalDevice(in android.media.audio.common.AudioPort templateIdAndAdditionalData);
+  void disconnectExternalDevice(int portId);
   android.hardware.audio.core.AudioPatch[] getAudioPatches();
   android.media.audio.common.AudioPort getAudioPort(int portId);
   android.media.audio.common.AudioPortConfig[] getAudioPortConfigs();
   android.media.audio.common.AudioPort[] getAudioPorts();
   android.hardware.audio.core.AudioRoute[] getAudioRoutes();
+  android.hardware.audio.core.AudioRoute[] getAudioRoutesForAudioPort(int portId);
   android.hardware.audio.core.IStreamIn openInputStream(int portConfigId, in android.hardware.audio.common.SinkMetadata sinkMetadata);
   android.hardware.audio.core.IStreamOut openOutputStream(int portConfigId, in android.hardware.audio.common.SourceMetadata sourceMetadata, in @nullable android.media.audio.common.AudioOffloadInfo offloadInfo);
   android.hardware.audio.core.AudioPatch setAudioPatch(in android.hardware.audio.core.AudioPatch requested);
diff --git a/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/ModuleDebug.aidl b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/ModuleDebug.aidl
new file mode 100644
index 0000000..80ee185
--- /dev/null
+++ b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/ModuleDebug.aidl
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2022 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.audio.core;
+@JavaDerive(equals=true, toString=true) @VintfStability
+parcelable ModuleDebug {
+  boolean simulateDeviceConnections;
+}
diff --git a/audio/aidl/android/hardware/audio/core/IModule.aidl b/audio/aidl/android/hardware/audio/core/IModule.aidl
index d47ea3c..f406cd8 100644
--- a/audio/aidl/android/hardware/audio/core/IModule.aidl
+++ b/audio/aidl/android/hardware/audio/core/IModule.aidl
@@ -22,6 +22,7 @@
 import android.hardware.audio.core.AudioRoute;
 import android.hardware.audio.core.IStreamIn;
 import android.hardware.audio.core.IStreamOut;
+import android.hardware.audio.core.ModuleDebug;
 import android.media.audio.common.AudioOffloadInfo;
 import android.media.audio.common.AudioPort;
 import android.media.audio.common.AudioPortConfig;
@@ -44,6 +45,116 @@
 @VintfStability
 interface IModule {
     /**
+     * Sets debugging configuration for the HAL module. This method is only
+     * called during xTS testing and is intended for validating the aspects of
+     * the HAL module behavior that would otherwise require human intervention.
+     *
+     * The HAL module must throw an error if there is an attempt to change
+     * the debug behavior for the aspect which is currently in use.
+     *
+     * @param debug The debug options.
+     * @throws EX_ILLEGAL_STATE If the flag(s) being changed affect functionality
+     *                          which is currently in use.
+     */
+    void setModuleDebug(in ModuleDebug debug);
+
+    /**
+     * Set a device port of an external device into connected state.
+     *
+     * This method is used to inform the HAL module that an external device has
+     * been connected to a device port selected using the 'id' field of the
+     * input AudioPort parameter. This device port must have dynamic profiles
+     * (an empty list of profiles). This port is further referenced to as "port
+     * template" because it acts as a template for creating a new instance of a
+     * "connected" device port which gets returned from this method.
+     *
+     * The input AudioPort parameter may contain any additional data obtained by
+     * the system side from other subsystems. The nature of data depends on the
+     * type of the connection. For example, for point-to-multipoint external
+     * device connections, the input parameter may contain the address of the
+     * connected external device. Another example is EDID information for HDMI
+     * connections (ExtraAudioDescriptor), which can be provided by the HDMI-CEC
+     * HAL module.
+     *
+     * It is the responsibility of the HAL module to query audio profiles
+     * supported by the connected device and return them as part of the returned
+     * AudioPort instance. In the case when the HAL is unable to query the
+     * external device, an error must be thrown.
+     *
+     * Thus, the returned audio port instance is the result of combining the
+     * following information:
+     *  - a unique port ID generated by the HAL module;
+     *  - static information from the port template;
+     *  - list of audio profiles supported by the connected device;
+     *  - additional data from the input AudioPort parameter.
+     *
+     * The HAL module must also update the list of audio routes to include the
+     * ID of the instantiated connected device port. Normally, the connected
+     * port allows the same routing as the port template.
+     *
+     * Also see notes on 'ModuleDebug.simulateDeviceConnections'.
+     *
+     * The following protocol is used by HAL module client for handling
+     * connection of an external device:
+     *  1. Obtain the list of device ports and their IDs via 'getAudioPorts'
+     *     method. Select the appropriate port template using
+     *     AudioDeviceDescription ('ext.device' field of AudioPort).
+     *  2. Combine the ID of the port template with any additional data and call
+     *     'connectExternalDevice'. The HAL module returns a new instance of
+     *     AudioPort created using the rules explained above. Both
+     *     'getAudioPort' and 'getAudioPorts' methods will be returning the same
+     *     information for this port until disconnection.
+     *  3. Configure the connected port with one of supported profiles using
+     *     'setAudioPortConfig'.
+     *  4. Query the list of AudioRoutes for the new AudioPort using
+     *     'getAudioRoutesForAudioPort' or 'getAudioRoutes' methods.
+     *
+     * External devices are distinguished by the connection type and device
+     * address. Calling this method multiple times to inform about connection of
+     * the same external device without disconnecting it first is an error.
+     *
+     * The HAL module must perform validation of the input parameter and throw
+     * an error if it is lacking required information, for example, when no
+     * device address is specified for a point-to-multipoint external device
+     * connection.
+     *
+     * Handling of a disconnect is done in a reverse order:
+     *  1. Reset port configuration using the 'resetAudioPortConfig' method.
+     *  2. Release the connected device port by calling the 'disconnectExternalDevice'
+     *     method. This also removes the audio routes associated with this
+     *     device port.
+     *
+     * @return New instance of an audio port for the connected external device.
+     * @param templateIdAndAdditionalData Specifies port template ID and any
+     *                                    additional data.
+     * @throws EX_ILLEGAL_ARGUMENT In the following cases:
+     *                             - If the template port can not be found by the ID.
+     *                             - If the template is not a device port, or
+     *                               it does not have dynamic profiles.
+     *                             - If the input parameter is lacking required
+     *                               information.
+     * @throws EX_ILLEGAL_STATE In the following cases:
+     *                          - If the HAL module is unable to query audio profiles.
+     *                          - If the external device has already been connected.
+     */
+    AudioPort connectExternalDevice(in AudioPort templateIdAndAdditionalData);
+
+    /**
+     * Set a device port of a an external device into disconnected state.
+     *
+     * This method is used to inform the HAL module that an external device has
+     * been disconnected. The 'portId' must be of a connected device port
+     * instance previously instantiated using the 'connectExternalDevice'
+     * method.
+     *
+     * @throws EX_ILLEGAL_ARGUMENT In the following cases:
+     *                             - If the port can not be found by the ID.
+     *                             - If this is not a connected device port.
+     * @throws EX_ILLEGAL_STATE If the port has active configurations.
+     */
+    void disconnectExternalDevice(int portId);
+
+    /**
      * Return all audio patches of this module.
      *
      * Returns a list of audio patches, that is, established connections between
@@ -57,12 +168,10 @@
      * Return the current state of the audio port.
      *
      * Using the port ID provided on input, returns the current state of the
-     * audio port. For device port representing a connection to some external
-     * device, e.g. over HDMI or USB, currently supported audio profiles and
-     * extra audio descriptors may change.
-     *
-     * For all other audio ports it must be the same configuration as returned
-     * for this port ID by 'getAudioPorts'.
+     * audio port. The values of the AudioPort structure must be the same as
+     * currently returned by the 'getAudioPorts' method. The 'getAudioPort'
+     * method is provided to reduce overhead in the case when the client needs
+     * to check the state of one port only.
      *
      * @return The current state of an audio port.
      * @param portId The ID of the audio port.
@@ -87,33 +196,45 @@
     AudioPortConfig[] getAudioPortConfigs();
 
     /**
-     * Return all audio ports provided by this module.
+     * Return the current state of all audio ports provided by this module.
      *
-     * Returns a list of all mix ports and device ports provided by this
-     * module. Each returned port must have a unique ID within this module
-     * ('AudioPort.id' field). The returned list must not change during
-     * the lifetime of the IModule instance. For audio ports with dynamic
-     * profiles (changing depending on external devices being connected
-     * to the system) an empty list of profiles must be returned. The list
-     * of currently supported audio profiles is obtained from 'getAudioPort'
-     * method.
+     * Returns a list of all mix ports and device ports provided by this HAL
+     * module, reflecting their current states. Each returned port must have a
+     * unique ID within this module ('AudioPort.id' field). The list also
+     * includes "connected" ports created using 'connectExternalDevice' method.
      *
      * @return The list of audio ports.
      */
     AudioPort[] getAudioPorts();
 
     /**
-     * Return all audio routes of this module.
+     * Return all current audio routes of this module.
      *
-     * Returns a list of audio routes, that is, allowed connections between
-     * audio ports. The returned list must not change during the lifetime of the
-     * IModule instance.
+     * Returns the current list of audio routes, that is, allowed connections
+     * between audio ports. The list can change when new device audio ports
+     * get created as a result of connecting or disconnecting of external
+     * devices.
      *
      * @return The list of audio routes.
      */
     AudioRoute[] getAudioRoutes();
 
     /**
+     * Return audio routes related to the specified audio port.
+     *
+     * Returns the list of audio routes that include the specified port ID
+     * as a source or as a sink. The returned list is a subset of the result
+     * returned by the 'getAudioRoutes' method, filtered by the port ID.
+     * An empty list can be returned, indicating that the audio port can not
+     * be used for creating audio patches.
+     *
+     * @return The list of audio routes.
+     * @param portId The ID of the audio port.
+     * @throws EX_ILLEGAL_ARGUMENT If the port can not be found by the ID.
+     */
+    AudioRoute[] getAudioRoutesForAudioPort(int portId);
+
+    /**
      * Open an input stream using an existing audio mix port configuration.
      *
      * The audio port configuration ID must be obtained by calling
@@ -230,6 +351,10 @@
      * parameter. The framework can then set the suggested configuration on a
      * subsequent retry call to this method.
      *
+     * Device ports with dynamic audio profiles (an empty list of profiles)
+     * can not be used with this method. The list of profiles must be filled in
+     * as a result of calling 'connectExternalDevice' method.
+     *
      * @return Whether the requested configuration has been applied.
      * @param requested Requested audio port configuration.
      * @param suggested Same as requested configuration, if it was applied.
@@ -241,7 +366,7 @@
      *                             - If the port can not be found by the port ID.
      *                             - If it is not possible to generate a suggested port
      *                               configuration, for example, if the port only has dynamic
-     *                               profiles and they are currently empty.
+     *                               profiles.
      */
     boolean setAudioPortConfig(in AudioPortConfig requested, out AudioPortConfig suggested);
 
diff --git a/audio/aidl/android/hardware/audio/core/ModuleDebug.aidl b/audio/aidl/android/hardware/audio/core/ModuleDebug.aidl
new file mode 100644
index 0000000..858a9bd
--- /dev/null
+++ b/audio/aidl/android/hardware/audio/core/ModuleDebug.aidl
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2022 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.audio.core;
+
+/**
+ * This structure contains flags used for enabling various debugging aspects
+ * in a HAL module. By default, all debugging aspects are turned off. They
+ * can be enabled during xTS tests for functionality that, for example, would
+ * otherwise require human intervention (e.g. connection of external devices).
+ */
+@JavaDerive(equals=true, toString=true)
+@VintfStability
+parcelable ModuleDebug {
+    /**
+     * When set to 'true', HAL module must simulate connection of external
+     * devices. An external device becomes 'connected' after a call to
+     * IModule.connectExternalDevice, simulation of connection requires:
+     *  - provision of at least one non-dynamic device port profile on
+     *    connection (as if it was retrieved from a connected device);
+     *  - simulating successful application of port configurations for reported
+     *    profiles.
+     */
+    boolean simulateDeviceConnections;
+}
diff --git a/audio/aidl/default/Android.bp b/audio/aidl/default/Android.bp
index 0a6fe60..4728a89 100644
--- a/audio/aidl/default/Android.bp
+++ b/audio/aidl/default/Android.bp
@@ -13,6 +13,7 @@
     shared_libs: [
         "libbase",
         "libbinder_ndk",
+        "android.media.audio.common.types-V1-ndk",
         "android.hardware.audio.core-V1-ndk",
     ],
     export_include_dirs: ["include"],
@@ -36,6 +37,7 @@
     shared_libs: [
         "libbase",
         "libbinder_ndk",
+        "android.media.audio.common.types-V1-ndk",
         "android.hardware.audio.core-V1-ndk",
     ],
     static_libs: [
diff --git a/audio/aidl/default/Configuration.cpp b/audio/aidl/default/Configuration.cpp
index 1104caa..19d0b3c 100644
--- a/audio/aidl/default/Configuration.cpp
+++ b/audio/aidl/default/Configuration.cpp
@@ -16,14 +16,15 @@
 
 #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/AudioIoFlags.h>
 #include <aidl/android/media/audio/common/AudioOutputFlags.h>
 
-#include "aidl/android/media/audio/common/AudioFormatDescription.h"
 #include "core-impl/Configuration.h"
 
 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;
@@ -54,9 +55,11 @@
     return profile;
 }
 
-static AudioPortExt createDeviceExt(AudioDeviceType devType, int32_t flags) {
+static AudioPortExt createDeviceExt(AudioDeviceType devType, int32_t flags,
+                                    std::string connection = "") {
     AudioPortDeviceExt deviceExt;
     deviceExt.device.type.type = devType;
+    deviceExt.device.type.connection = std::move(connection);
     deviceExt.flags = flags;
     return AudioPortExt::make<AudioPortExt::Tag::device>(deviceExt);
 }
@@ -102,8 +105,64 @@
     return route;
 }
 
+// Configuration:
+//
+// Device ports:
+//  * "Null", OUT_SPEAKER, default
+//    - no profiles specified
+//  * "Loopback Out", OUT_SUBMIX
+//    - profile PCM 24-bit; STEREO; 48000
+//  * "USB Out", OUT_DEVICE, CONNECTION_USB
+//    - no profiles specified
+//  * "Zero", IN_MICROPHONE, default
+//    - no profiles specified
+//  * "Loopback In", IN_SUBMIX
+//    - profile PCM 24-bit; STEREO; 48000
+//  * "USB In", IN_DEVICE, CONNECTION_USB
+//    - no profiles specified
+//
+// Mix ports:
+//  * "primary output", PRIMARY, 1 max open, 1 max active stream
+//    - profile PCM 16-bit; MONO, STEREO; 44100, 48000
+//    - profile PCM 24-bit; MONO, STEREO; 44100, 48000
+//  * "loopback output", stream count unlimited
+//    - profile PCM 24-bit; STEREO; 48000
+//  * "primary input", 2 max open, 2 max active streams
+//    - profile PCM 16-bit; MONO, STEREO, FRONT_BACK;
+//        8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000
+//    - profile PCM 24-bit; MONO, STEREO, FRONT_BACK;
+//        8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000
+//  * "loopback input", stream count unlimited
+//    - profile PCM 24-bit; STEREO; 48000
+//
+// Routes:
+//  "primary out" -> "Null"
+//  "primary out" -> "USB Out"
+//  "loopback out" -> "Loopback Out"
+//  "Zero", "USB In" -> "primary input"
+//  "Loopback In" -> "loopback input"
+//
+// Initial port configs:
+//  * "Null" device port: PCM 24-bit; STEREO; 48000
+//  * "Zero" device port: PCM 24-bit; MONO; 48000
+//
+// Profiles for device port connected state:
+//  * USB Out":
+//    - profile PCM 16-bit; MONO, STEREO; 44100, 48000
+//    - profile PCM 24-bit; MONO, STEREO; 44100, 48000
+//  * USB In":
+//    - profile PCM 16-bit; MONO, STEREO; 44100, 48000
+//    - profile PCM 24-bit; MONO, STEREO; 44100, 48000
+//
 Configuration& getNullPrimaryConfiguration() {
     static Configuration configuration = []() {
+        const std::vector<AudioProfile> standardPcmAudioProfiles = {
+                createProfile(PcmType::INT_16_BIT,
+                              {AudioChannelLayout::LAYOUT_MONO, AudioChannelLayout::LAYOUT_STEREO},
+                              {44100, 48000}),
+                createProfile(PcmType::INT_24_BIT,
+                              {AudioChannelLayout::LAYOUT_MONO, AudioChannelLayout::LAYOUT_STEREO},
+                              {44100, 48000})};
         Configuration c;
 
         AudioPort nullOutDevice =
@@ -111,27 +170,19 @@
                            createDeviceExt(AudioDeviceType::OUT_SPEAKER,
                                            1 << AudioPortDeviceExt::FLAG_INDEX_DEFAULT_DEVICE));
         c.ports.push_back(nullOutDevice);
-
-        AudioPort primaryOutMix = createPort(c.nextPortId++, "primary output",
-                                             1 << static_cast<int32_t>(AudioOutputFlags::PRIMARY),
-                                             false, createPortMixExt(1, 1));
-        primaryOutMix.profiles.push_back(
-                createProfile(PcmType::INT_16_BIT,
-                              {AudioChannelLayout::LAYOUT_MONO, AudioChannelLayout::LAYOUT_STEREO},
-                              {44100, 48000}));
-        primaryOutMix.profiles.push_back(
-                createProfile(PcmType::INT_24_BIT,
-                              {AudioChannelLayout::LAYOUT_MONO, AudioChannelLayout::LAYOUT_STEREO},
-                              {44100, 48000}));
-        c.ports.push_back(primaryOutMix);
-
-        c.routes.push_back(createRoute({primaryOutMix.id}, nullOutDevice.id));
-
         c.initialConfigs.push_back(
                 createPortConfig(nullOutDevice.id, nullOutDevice.id, PcmType::INT_24_BIT,
                                  AudioChannelLayout::LAYOUT_STEREO, 48000, 0, false,
                                  createDeviceExt(AudioDeviceType::OUT_SPEAKER, 0)));
 
+        AudioPort primaryOutMix = createPort(c.nextPortId++, "primary output",
+                                             1 << static_cast<int32_t>(AudioOutputFlags::PRIMARY),
+                                             false, createPortMixExt(1, 1));
+        primaryOutMix.profiles.insert(primaryOutMix.profiles.begin(),
+                                      standardPcmAudioProfiles.begin(),
+                                      standardPcmAudioProfiles.end());
+        c.ports.push_back(primaryOutMix);
+
         AudioPort loopOutDevice = createPort(c.nextPortId++, "Loopback Out", 0, false,
                                              createDeviceExt(AudioDeviceType::OUT_SUBMIX, 0));
         loopOutDevice.profiles.push_back(
@@ -144,13 +195,22 @@
                 createProfile(PcmType::INT_24_BIT, {AudioChannelLayout::LAYOUT_STEREO}, {48000}));
         c.ports.push_back(loopOutMix);
 
-        c.routes.push_back(createRoute({loopOutMix.id}, loopOutDevice.id));
+        AudioPort usbOutDevice =
+                createPort(c.nextPortId++, "USB Out", 0, false,
+                           createDeviceExt(AudioDeviceType::OUT_DEVICE, 0,
+                                           AudioDeviceDescription::CONNECTION_USB));
+        c.ports.push_back(usbOutDevice);
+        c.connectedProfiles[usbOutDevice.id] = standardPcmAudioProfiles;
 
         AudioPort zeroInDevice =
                 createPort(c.nextPortId++, "Zero", 0, true,
                            createDeviceExt(AudioDeviceType::IN_MICROPHONE,
                                            1 << AudioPortDeviceExt::FLAG_INDEX_DEFAULT_DEVICE));
         c.ports.push_back(zeroInDevice);
+        c.initialConfigs.push_back(
+                createPortConfig(zeroInDevice.id, zeroInDevice.id, PcmType::INT_24_BIT,
+                                 AudioChannelLayout::LAYOUT_MONO, 48000, 0, true,
+                                 createDeviceExt(AudioDeviceType::IN_MICROPHONE, 0)));
 
         AudioPort primaryInMix =
                 createPort(c.nextPortId++, "primary input", 0, true, createPortMixExt(2, 2));
@@ -166,13 +226,6 @@
                               {8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000}));
         c.ports.push_back(primaryInMix);
 
-        c.routes.push_back(createRoute({zeroInDevice.id}, primaryInMix.id));
-
-        c.initialConfigs.push_back(
-                createPortConfig(zeroInDevice.id, zeroInDevice.id, PcmType::INT_24_BIT,
-                                 AudioChannelLayout::LAYOUT_MONO, 48000, 0, true,
-                                 createDeviceExt(AudioDeviceType::IN_MICROPHONE, 0)));
-
         AudioPort loopInDevice = createPort(c.nextPortId++, "Loopback In", 0, true,
                                             createDeviceExt(AudioDeviceType::IN_SUBMIX, 0));
         loopInDevice.profiles.push_back(
@@ -185,6 +238,16 @@
                 createProfile(PcmType::INT_24_BIT, {AudioChannelLayout::LAYOUT_STEREO}, {48000}));
         c.ports.push_back(loopInMix);
 
+        AudioPort usbInDevice = createPort(c.nextPortId++, "USB In", 0, true,
+                                           createDeviceExt(AudioDeviceType::IN_DEVICE, 0,
+                                                           AudioDeviceDescription::CONNECTION_USB));
+        c.ports.push_back(usbInDevice);
+        c.connectedProfiles[usbInDevice.id] = standardPcmAudioProfiles;
+
+        c.routes.push_back(createRoute({primaryOutMix.id}, nullOutDevice.id));
+        c.routes.push_back(createRoute({primaryOutMix.id}, usbOutDevice.id));
+        c.routes.push_back(createRoute({loopOutMix.id}, loopOutDevice.id));
+        c.routes.push_back(createRoute({zeroInDevice.id, usbInDevice.id}, primaryInMix.id));
         c.routes.push_back(createRoute({loopInDevice.id}, loopInMix.id));
 
         c.portConfigs.insert(c.portConfigs.end(), c.initialConfigs.begin(), c.initialConfigs.end());
diff --git a/audio/aidl/default/Module.cpp b/audio/aidl/default/Module.cpp
index e0a68a5..961ee84 100644
--- a/audio/aidl/default/Module.cpp
+++ b/audio/aidl/default/Module.cpp
@@ -18,7 +18,6 @@
 #include <set>
 
 #define LOG_TAG "AHAL_Module"
-#define LOG_NDEBUG 0
 #include <android-base/logging.h>
 
 #include <aidl/android/media/audio/common/AudioOutputFlags.h>
@@ -81,6 +80,7 @@
     }
     return false;
 }
+
 }  // namespace
 
 void Module::cleanUpPatch(int32_t patchId) {
@@ -135,6 +135,154 @@
     do_insert(patch.sinkPortConfigIds);
 }
 
+ndk::ScopedAStatus Module::setModuleDebug(
+        const ::aidl::android::hardware::audio::core::ModuleDebug& in_debug) {
+    LOG(DEBUG) << __func__ << ": old flags:" << mDebug.toString()
+               << ", new flags: " << in_debug.toString();
+    if (mDebug.simulateDeviceConnections != in_debug.simulateDeviceConnections &&
+        !mConnectedDevicePorts.empty()) {
+        LOG(ERROR) << __func__ << ": attempting to change device connections simulation "
+                   << "while having external devices connected";
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+    }
+    mDebug = in_debug;
+    return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus Module::connectExternalDevice(const AudioPort& in_templateIdAndAdditionalData,
+                                                 AudioPort* _aidl_return) {
+    const int32_t templateId = in_templateIdAndAdditionalData.id;
+    auto& ports = getConfig().ports;
+    AudioPort connectedPort;
+    {  // Scope the template port so that we don't accidentally modify it.
+        auto templateIt = findById<AudioPort>(ports, templateId);
+        if (templateIt == ports.end()) {
+            LOG(ERROR) << __func__ << ": port id " << templateId << " not found";
+            return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+        }
+        if (templateIt->ext.getTag() != AudioPortExt::Tag::device) {
+            LOG(ERROR) << __func__ << ": port id " << templateId << " is not a device port";
+            return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+        }
+        if (!templateIt->profiles.empty()) {
+            LOG(ERROR) << __func__ << ": port id " << templateId
+                       << " does not have dynamic profiles";
+            return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+        }
+        auto& templateDevicePort = templateIt->ext.get<AudioPortExt::Tag::device>();
+        if (templateDevicePort.device.type.connection.empty()) {
+            LOG(ERROR) << __func__ << ": port id " << templateId << " is permanently attached";
+            return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+        }
+        // Postpone id allocation until we ensure that there are no client errors.
+        connectedPort = *templateIt;
+        connectedPort.extraAudioDescriptors = in_templateIdAndAdditionalData.extraAudioDescriptors;
+        const auto& inputDevicePort =
+                in_templateIdAndAdditionalData.ext.get<AudioPortExt::Tag::device>();
+        auto& connectedDevicePort = connectedPort.ext.get<AudioPortExt::Tag::device>();
+        connectedDevicePort.device.address = inputDevicePort.device.address;
+        LOG(DEBUG) << __func__ << ": device port " << connectedPort.id << " device set to "
+                   << connectedDevicePort.device.toString();
+        // Check if there is already a connected port with for the same external device.
+        for (auto connectedPortId : mConnectedDevicePorts) {
+            auto connectedPortIt = findById<AudioPort>(ports, connectedPortId);
+            if (connectedPortIt->ext.get<AudioPortExt::Tag::device>().device ==
+                connectedDevicePort.device) {
+                LOG(ERROR) << __func__ << ": device " << connectedDevicePort.device.toString()
+                           << " is already connected at the device port id " << connectedPortId;
+                return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+            }
+        }
+    }
+
+    if (!mDebug.simulateDeviceConnections) {
+        // In a real HAL here we would attempt querying the profiles from the device.
+        LOG(ERROR) << __func__ << ": failed to query supported device profiles";
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+    }
+
+    connectedPort.id = ++getConfig().nextPortId;
+    mConnectedDevicePorts.insert(connectedPort.id);
+    LOG(DEBUG) << __func__ << ": template port " << templateId << " external device connected, "
+               << "connected port ID " << connectedPort.id;
+    auto& connectedProfiles = getConfig().connectedProfiles;
+    if (auto connectedProfilesIt = connectedProfiles.find(templateId);
+        connectedProfilesIt != connectedProfiles.end()) {
+        connectedPort.profiles = connectedProfilesIt->second;
+    }
+    ports.push_back(connectedPort);
+    *_aidl_return = std::move(connectedPort);
+
+    std::vector<AudioRoute> newRoutes;
+    auto& routes = getConfig().routes;
+    for (auto& r : routes) {
+        if (r.sinkPortId == templateId) {
+            AudioRoute newRoute;
+            newRoute.sourcePortIds = r.sourcePortIds;
+            newRoute.sinkPortId = connectedPort.id;
+            newRoute.isExclusive = r.isExclusive;
+            newRoutes.push_back(std::move(newRoute));
+        } else {
+            auto& srcs = r.sourcePortIds;
+            if (std::find(srcs.begin(), srcs.end(), templateId) != srcs.end()) {
+                srcs.push_back(connectedPort.id);
+            }
+        }
+    }
+    routes.insert(routes.end(), newRoutes.begin(), newRoutes.end());
+
+    return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus Module::disconnectExternalDevice(int32_t in_portId) {
+    auto& ports = getConfig().ports;
+    auto portIt = findById<AudioPort>(ports, in_portId);
+    if (portIt == ports.end()) {
+        LOG(ERROR) << __func__ << ": port id " << in_portId << " not found";
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+    }
+    if (portIt->ext.getTag() != AudioPortExt::Tag::device) {
+        LOG(ERROR) << __func__ << ": port id " << in_portId << " is not a device port";
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+    }
+    if (mConnectedDevicePorts.count(in_portId) == 0) {
+        LOG(ERROR) << __func__ << ": port id " << in_portId << " is not a connected device port";
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+    }
+    auto& configs = getConfig().portConfigs;
+    auto& initials = getConfig().initialConfigs;
+    auto configIt = std::find_if(configs.begin(), configs.end(), [&](const auto& config) {
+        if (config.portId == in_portId) {
+            // Check if the configuration was provided by the client.
+            const auto& initialIt = findById<AudioPortConfig>(initials, config.id);
+            return initialIt == initials.end() || config != *initialIt;
+        }
+        return false;
+    });
+    if (configIt != configs.end()) {
+        LOG(ERROR) << __func__ << ": port id " << in_portId << " has a non-default config with id "
+                   << configIt->id;
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+    }
+    ports.erase(portIt);
+    mConnectedDevicePorts.erase(in_portId);
+    LOG(DEBUG) << __func__ << ": connected device port " << in_portId << " released";
+
+    auto& routes = getConfig().routes;
+    for (auto routesIt = routes.begin(); routesIt != routes.end();) {
+        if (routesIt->sinkPortId == in_portId) {
+            routesIt = routes.erase(routesIt);
+        } else {
+            // Note: the list of sourcePortIds can't become empty because there must
+            // be the id of the template port in the route.
+            erase_if(routesIt->sourcePortIds, [in_portId](auto src) { return src == in_portId; });
+            ++routesIt;
+        }
+    }
+
+    return ndk::ScopedAStatus::ok();
+}
+
 ndk::ScopedAStatus Module::getAudioPatches(std::vector<AudioPatch>* _aidl_return) {
     *_aidl_return = getConfig().patches;
     LOG(DEBUG) << __func__ << ": returning " << _aidl_return->size() << " patches";
@@ -171,6 +319,23 @@
     return ndk::ScopedAStatus::ok();
 }
 
+ndk::ScopedAStatus Module::getAudioRoutesForAudioPort(int32_t in_portId,
+                                                      std::vector<AudioRoute>* _aidl_return) {
+    auto& ports = getConfig().ports;
+    if (auto portIt = findById<AudioPort>(ports, in_portId); portIt == ports.end()) {
+        LOG(ERROR) << __func__ << ": port id " << in_portId << " not found";
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+    }
+    auto& routes = getConfig().routes;
+    std::copy_if(routes.begin(), routes.end(), std::back_inserter(*_aidl_return),
+                 [&](const auto& r) {
+                     const auto& srcs = r.sourcePortIds;
+                     return r.sinkPortId == in_portId ||
+                            std::find(srcs.begin(), srcs.end(), in_portId) != srcs.end();
+                 });
+    return ndk::ScopedAStatus::ok();
+}
+
 ndk::ScopedAStatus Module::openInputStream(int32_t in_portConfigId,
                                            const SinkMetadata& in_sinkMetadata,
                                            std::shared_ptr<IStreamIn>* _aidl_return) {
diff --git a/audio/aidl/default/include/core-impl/Configuration.h b/audio/aidl/default/include/core-impl/Configuration.h
index 17e342d..d5cd30b 100644
--- a/audio/aidl/default/include/core-impl/Configuration.h
+++ b/audio/aidl/default/include/core-impl/Configuration.h
@@ -16,6 +16,7 @@
 
 #pragma once
 
+#include <map>
 #include <vector>
 
 #include <aidl/android/hardware/audio/core/AudioPatch.h>
@@ -29,6 +30,9 @@
     std::vector<::aidl::android::media::audio::common::AudioPort> ports;
     std::vector<::aidl::android::media::audio::common::AudioPortConfig> portConfigs;
     std::vector<::aidl::android::media::audio::common::AudioPortConfig> initialConfigs;
+    // Port id -> List of profiles to use when the device port state is set to 'connected'.
+    std::map<int32_t, std::vector<::aidl::android::media::audio::common::AudioProfile>>
+            connectedProfiles;
     std::vector<AudioRoute> routes;
     std::vector<AudioPatch> patches;
     int32_t nextPortId = 1;
diff --git a/audio/aidl/default/include/core-impl/Module.h b/audio/aidl/default/include/core-impl/Module.h
index 359626c..81a02ba 100644
--- a/audio/aidl/default/include/core-impl/Module.h
+++ b/audio/aidl/default/include/core-impl/Module.h
@@ -18,6 +18,7 @@
 
 #include <map>
 #include <memory>
+#include <set>
 
 #include <aidl/android/hardware/audio/core/BnModule.h>
 
@@ -27,6 +28,12 @@
 namespace aidl::android::hardware::audio::core {
 
 class Module : public BnModule {
+    ndk::ScopedAStatus setModuleDebug(
+            const ::aidl::android::hardware::audio::core::ModuleDebug& in_debug) override;
+    ndk::ScopedAStatus connectExternalDevice(
+            const ::aidl::android::media::audio::common::AudioPort& in_templateIdAndAdditionalData,
+            ::aidl::android::media::audio::common::AudioPort* _aidl_return) override;
+    ndk::ScopedAStatus disconnectExternalDevice(int32_t in_portId) override;
     ndk::ScopedAStatus getAudioPatches(std::vector<AudioPatch>* _aidl_return) override;
     ndk::ScopedAStatus getAudioPort(
             int32_t in_portId,
@@ -37,6 +44,9 @@
     ndk::ScopedAStatus getAudioPorts(
             std::vector<::aidl::android::media::audio::common::AudioPort>* _aidl_return) override;
     ndk::ScopedAStatus getAudioRoutes(std::vector<AudioRoute>* _aidl_return) override;
+    ndk::ScopedAStatus getAudioRoutesForAudioPort(
+            int32_t in_portId,
+            std::vector<::aidl::android::hardware::audio::core::AudioRoute>* _aidl_return) override;
     ndk::ScopedAStatus openInputStream(
             int32_t in_portConfigId,
             const ::aidl::android::hardware::audio::common::SinkMetadata& in_sinkMetadata,
@@ -63,6 +73,9 @@
     void registerPatch(const AudioPatch& patch);
 
     std::unique_ptr<internal::Configuration> mConfig;
+    ModuleDebug mDebug;
+    // ids of ports created at runtime via 'connectExternalDevice'.
+    std::set<int32_t> mConnectedDevicePorts;
     Streams mStreams;
     // Maps port ids and port config ids to patch ids.
     // Multimap because both ports and configs can be used by multiple patches.
diff --git a/audio/aidl/default/include/core-impl/utils.h b/audio/aidl/default/include/core-impl/utils.h
index 7101012..9d06f08 100644
--- a/audio/aidl/default/include/core-impl/utils.h
+++ b/audio/aidl/default/include/core-impl/utils.h
@@ -38,11 +38,11 @@
     return oldSize - c.size();
 }
 
-// Erase all the elements in the map that satisfy the provided predicate.
+// Erase all the elements in the container that satisfy the provided predicate.
 template <typename C, typename P>
 auto erase_if(C& c, P pred) {
     auto oldSize = c.size();
-    for (auto it = c.begin(), last = c.end(); it != last;) {
+    for (auto it = c.begin(); it != c.end();) {
         if (pred(*it)) {
             it = c.erase(it);
         } else {
diff --git a/audio/aidl/vts/ModuleConfig.cpp b/audio/aidl/vts/ModuleConfig.cpp
index 3faa39a..3f8d088 100644
--- a/audio/aidl/vts/ModuleConfig.cpp
+++ b/audio/aidl/vts/ModuleConfig.cpp
@@ -46,14 +46,16 @@
     for (const auto& port : mPorts) {
         if (port.ext.getTag() != AudioPortExt::Tag::device) continue;
         const auto& devicePort = port.ext.get<AudioPortExt::Tag::device>();
-        const bool isInput = port.flags.getTag() == AudioIoFlags::Tag::input;
         if (devicePort.device.type.connection.empty()) {
+            const bool isInput = port.flags.getTag() == AudioIoFlags::Tag::input;
             // Permanently attached device.
             if (isInput) {
                 mAttachedSourceDevicePorts.insert(port.id);
             } else {
                 mAttachedSinkDevicePorts.insert(port.id);
             }
+        } else if (port.profiles.empty()) {
+            mExternalDevicePorts.insert(port.id);
         }
     }
     if (!mStatus.isOk()) return;
@@ -62,6 +64,22 @@
     mStatus = module->getAudioPortConfigs(&mInitialConfigs);
 }
 
+std::vector<AudioPort> ModuleConfig::getAttachedDevicePorts() const {
+    std::vector<AudioPort> result;
+    std::copy_if(mPorts.begin(), mPorts.end(), std::back_inserter(result), [&](const auto& port) {
+        return mAttachedSinkDevicePorts.count(port.id) != 0 ||
+               mAttachedSourceDevicePorts.count(port.id) != 0;
+    });
+    return result;
+}
+
+std::vector<AudioPort> ModuleConfig::getExternalDevicePorts() const {
+    std::vector<AudioPort> result;
+    std::copy_if(mPorts.begin(), mPorts.end(), std::back_inserter(result),
+                 [&](const auto& port) { return mExternalDevicePorts.count(port.id) != 0; });
+    return result;
+}
+
 std::vector<AudioPort> ModuleConfig::getInputMixPorts() const {
     std::vector<AudioPort> result;
     std::copy_if(mPorts.begin(), mPorts.end(), std::back_inserter(result), [](const auto& port) {
@@ -229,6 +247,23 @@
     return result;
 }
 
+std::string ModuleConfig::toString() const {
+    std::string result;
+    result.append("Ports: ");
+    result.append(android::internal::ToString(mPorts));
+    result.append("Initial configs: ");
+    result.append(android::internal::ToString(mInitialConfigs));
+    result.append("Attached sink device ports: ");
+    result.append(android::internal::ToString(mAttachedSinkDevicePorts));
+    result.append("Attached source device ports: ");
+    result.append(android::internal::ToString(mAttachedSourceDevicePorts));
+    result.append("External device ports: ");
+    result.append(android::internal::ToString(mExternalDevicePorts));
+    result.append("Routes: ");
+    result.append(android::internal::ToString(mRoutes));
+    return result;
+}
+
 static std::vector<AudioPortConfig> combineAudioConfigs(const AudioPort& port,
                                                         const AudioProfile& profile) {
     std::vector<AudioPortConfig> configs;
@@ -319,10 +354,14 @@
             if (singleProfile && !result.empty()) return result;
         }
         if (resultSizeBefore == result.size()) {
-            AudioPortConfig empty;
-            empty.portId = devicePort.id;
-            empty.ext = devicePort.ext;
-            result.push_back(empty);
+            std::copy_if(mInitialConfigs.begin(), mInitialConfigs.end(), std::back_inserter(result),
+                         [&](const auto& config) { return config.portId == devicePort.id; });
+            if (resultSizeBefore == result.size()) {
+                AudioPortConfig empty;
+                empty.portId = devicePort.id;
+                empty.ext = devicePort.ext;
+                result.push_back(empty);
+            }
         }
         if (singleProfile) return result;
     }
diff --git a/audio/aidl/vts/ModuleConfig.h b/audio/aidl/vts/ModuleConfig.h
index 2e86b97..0e2738b 100644
--- a/audio/aidl/vts/ModuleConfig.h
+++ b/audio/aidl/vts/ModuleConfig.h
@@ -37,6 +37,8 @@
     android::binder::Status getStatus() const { return mStatus; }
     std::string getError() const { return mStatus.toString8().c_str(); }
 
+    std::vector<android::media::audio::common::AudioPort> getAttachedDevicePorts() const;
+    std::vector<android::media::audio::common::AudioPort> getExternalDevicePorts() const;
     std::vector<android::media::audio::common::AudioPort> getInputMixPorts() const;
     std::vector<android::media::audio::common::AudioPort> getOutputMixPorts() const;
     std::vector<android::media::audio::common::AudioPort> getMixPorts(bool isInput) const {
@@ -59,6 +61,10 @@
     std::optional<SrcSinkPair> getRoutableSrcSinkPair(bool isInput) const;
     std::vector<SrcSinkGroup> getRoutableSrcSinkGroups(bool isInput) const;
 
+    std::vector<android::media::audio::common::AudioPortConfig>
+    getPortConfigsForAttachedDevicePorts() const {
+        return generateAudioDevicePortConfigs(getAttachedDevicePorts(), false);
+    }
     std::vector<android::media::audio::common::AudioPortConfig> getPortConfigsForMixPorts() const {
         auto inputs = generateInputAudioMixPortConfigs(getInputMixPorts(), false);
         auto outputs = generateOutputAudioMixPortConfigs(getOutputMixPorts(), false);
@@ -98,15 +104,18 @@
         }
     }
 
+    std::vector<android::media::audio::common::AudioPortConfig> getPortConfigsForDevicePort(
+            const android::media::audio::common::AudioPort& port) const {
+        return generateAudioDevicePortConfigs({port}, false);
+    }
     android::media::audio::common::AudioPortConfig getSingleConfigForDevicePort(
             const android::media::audio::common::AudioPort& port) const {
-        for (const auto& config : mInitialConfigs) {
-            if (config.portId == port.id) return config;
-        }
         const auto config = generateAudioDevicePortConfigs({port}, true);
         return *config.begin();
     }
 
+    std::string toString() const;
+
   private:
     std::vector<android::media::audio::common::AudioPortConfig> generateInputAudioMixPortConfigs(
             const std::vector<android::media::audio::common::AudioPort>& ports,
@@ -117,7 +126,8 @@
 
     // Unlike MixPorts, the generator for DevicePorts always returns a non-empty
     // vector for a non-empty input port list. If there are no profiles in the
-    // port, a vector with an empty config is returned.
+    // port, its initial configs are looked up, if there are none,
+    // then an empty config is used, assuming further negotiation via setAudioPortConfig.
     std::vector<android::media::audio::common::AudioPortConfig> generateAudioDevicePortConfigs(
             const std::vector<android::media::audio::common::AudioPort>& ports,
             bool singleProfile) const;
@@ -127,5 +137,6 @@
     std::vector<android::media::audio::common::AudioPortConfig> mInitialConfigs;
     std::set<int32_t> mAttachedSinkDevicePorts;
     std::set<int32_t> mAttachedSourceDevicePorts;
+    std::set<int32_t> mExternalDevicePorts;
     std::vector<android::hardware::audio::core::AudioRoute> mRoutes;
 };
diff --git a/audio/aidl/vts/VtsHalAudioCoreTargetTest.cpp b/audio/aidl/vts/VtsHalAudioCoreTargetTest.cpp
index cadeb0c..824ac8d 100644
--- a/audio/aidl/vts/VtsHalAudioCoreTargetTest.cpp
+++ b/audio/aidl/vts/VtsHalAudioCoreTargetTest.cpp
@@ -22,6 +22,9 @@
 #include <set>
 #include <string>
 
+#define LOG_TAG "VtsHalAudioCore"
+#include <android-base/logging.h>
+
 #include <aidl/Gtest.h>
 #include <aidl/Vintf.h>
 #include <android-base/properties.h>
@@ -45,9 +48,12 @@
 using android::hardware::audio::core::IModule;
 using android::hardware::audio::core::IStreamIn;
 using android::hardware::audio::core::IStreamOut;
+using android::hardware::audio::core::ModuleDebug;
 using android::media::audio::common::AudioContentType;
 using android::media::audio::common::AudioDevice;
+using android::media::audio::common::AudioDeviceAddress;
 using android::media::audio::common::AudioDeviceType;
+using android::media::audio::common::AudioFormatType;
 using android::media::audio::common::AudioIoFlags;
 using android::media::audio::common::AudioOutputFlags;
 using android::media::audio::common::AudioPort;
@@ -63,7 +69,7 @@
 }
 
 template <typename C>
-std::vector<int32_t> getNonExistentIds(const C& allIds) {
+std::vector<int32_t> GetNonExistentIds(const C& allIds) {
     if (allIds.empty()) {
         return std::vector<int32_t>{-1, 0, 1};
     }
@@ -73,6 +79,12 @@
     return nonExistentIds;
 }
 
+AudioDeviceAddress GenerateUniqueDeviceAddress() {
+    static int nextId = 1;
+    // TODO: Use connection-specific ID.
+    return AudioDeviceAddress::make<AudioDeviceAddress::Tag::id>(std::to_string(++nextId));
+}
+
 struct AidlDeathRecipient : IBinder::DeathRecipient {
     std::mutex mutex;
     std::condition_variable condition;
@@ -107,70 +119,28 @@
     return false;
 }
 
-class AudioCoreModule : public testing::TestWithParam<std::string> {
+class WithDebugFlags {
   public:
-    void SetUp() override { ASSERT_NO_FATAL_FAILURE(ConnectToService()); }
-
-    void ConnectToService() {
-        module = android::waitForDeclaredService<IModule>(String16(GetParam().c_str()));
-        ASSERT_NE(module, nullptr);
-    }
-
-    void RestartService() {
-        ASSERT_NE(module, nullptr);
-        moduleConfig.reset();
-        deathHandler = sp<AidlDeathRecipient>::make();
-        ASSERT_EQ(NO_ERROR, IModule::asBinder(module)->linkToDeath(deathHandler));
-        ASSERT_TRUE(base::SetProperty("sys.audio.restart.hal", "1"));
-        EXPECT_TRUE(deathHandler->waitForFired(3000));
-        deathHandler = nullptr;
-        ASSERT_NO_FATAL_FAILURE(ConnectToService());
-    }
-
-    template <typename Entity>
-    void GetAllEntityIds(std::set<int32_t>* entityIds,
-                         Status (IModule::*getter)(std::vector<Entity>*),
-                         const std::string& errorMessage) {
-        std::vector<Entity> entities;
-        {
-            Status status = (module.get()->*getter)(&entities);
-            ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status;
-        }
-        std::transform(entities.begin(), entities.end(),
-                       std::inserter(*entityIds, entityIds->begin()),
-                       [](const auto& entity) { return entity.id; });
-        EXPECT_EQ(entities.size(), entityIds->size()) << errorMessage;
-    }
-
-    void GetAllPatchIds(std::set<int32_t>* patchIds) {
-        return GetAllEntityIds<AudioPatch>(
-                patchIds, &IModule::getAudioPatches,
-                "IDs of audio patches returned by IModule.getAudioPatches are not unique");
-    }
-
-    void GetAllPortIds(std::set<int32_t>* portIds) {
-        return GetAllEntityIds<AudioPort>(
-                portIds, &IModule::getAudioPorts,
-                "IDs of audio ports returned by IModule.getAudioPorts are not unique");
-    }
-
-    void GetAllPortConfigIds(std::set<int32_t>* portConfigIds) {
-        return GetAllEntityIds<AudioPortConfig>(
-                portConfigIds, &IModule::getAudioPortConfigs,
-                "IDs of audio port configs returned by IModule.getAudioPortConfigs are not unique");
-    }
-
-    void SetUpModuleConfig() {
-        if (moduleConfig == nullptr) {
-            moduleConfig = std::make_unique<ModuleConfig>(module.get());
-            ASSERT_EQ(Status::EX_NONE, moduleConfig->getStatus().exceptionCode())
-                    << "ModuleConfig init error: " << moduleConfig->getError();
+    WithDebugFlags() {}
+    explicit WithDebugFlags(const ModuleDebug& initial) : mInitial(initial), mFlags(initial) {}
+    explicit WithDebugFlags(const WithDebugFlags& initial)
+        : mInitial(initial.mFlags), mFlags(initial.mFlags) {}
+    ~WithDebugFlags() {
+        if (mModule != nullptr) {
+            Status status = mModule->setModuleDebug(mInitial);
+            EXPECT_EQ(Status::EX_NONE, status.exceptionCode()) << status;
         }
     }
+    void SetUp(IModule* module) {
+        Status status = module->setModuleDebug(mFlags);
+        ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status;
+    }
+    ModuleDebug& flags() { return mFlags; }
 
-    sp<IModule> module;
-    sp<AidlDeathRecipient> deathHandler;
-    std::unique_ptr<ModuleConfig> moduleConfig;
+  private:
+    ModuleDebug mInitial;
+    ModuleDebug mFlags;
+    IModule* mModule = nullptr;
 };
 
 // For consistency, WithAudioPortConfig can start both with a non-existent
@@ -226,6 +196,147 @@
     AudioPortConfig mConfig;
 };
 
+class AudioCoreModule : public testing::TestWithParam<std::string> {
+  public:
+    void SetUp() override {
+        ASSERT_NO_FATAL_FAILURE(ConnectToService());
+        debug.flags().simulateDeviceConnections = true;
+        ASSERT_NO_FATAL_FAILURE(debug.SetUp(module.get()));
+    }
+
+    void TearDown() override {
+        if (module != nullptr) {
+            Status status = module->setModuleDebug(ModuleDebug{});
+            EXPECT_EQ(Status::EX_NONE, status.exceptionCode())
+                    << status << " returned when resetting debug flags";
+        }
+    }
+
+    void ConnectToService() {
+        module = android::waitForDeclaredService<IModule>(String16(GetParam().c_str()));
+        ASSERT_NE(module, nullptr);
+    }
+
+    void RestartService() {
+        ASSERT_NE(module, nullptr);
+        moduleConfig.reset();
+        deathHandler = sp<AidlDeathRecipient>::make();
+        ASSERT_EQ(NO_ERROR, IModule::asBinder(module)->linkToDeath(deathHandler));
+        ASSERT_TRUE(base::SetProperty("sys.audio.restart.hal", "1"));
+        EXPECT_TRUE(deathHandler->waitForFired(3000));
+        deathHandler = nullptr;
+        ASSERT_NO_FATAL_FAILURE(ConnectToService());
+    }
+
+    void ApplyEveryConfig(const std::vector<AudioPortConfig>& configs) {
+        for (const auto& config : configs) {
+            ASSERT_NE(0, config.portId);
+            WithAudioPortConfig portConfig(config);
+            ASSERT_NO_FATAL_FAILURE(portConfig.SetUp(module.get()));  // calls setAudioPortConfig
+            EXPECT_EQ(config.portId, portConfig.get().portId);
+            std::vector<AudioPortConfig> retrievedPortConfigs;
+            Status status = module->getAudioPortConfigs(&retrievedPortConfigs);
+            ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status;
+            const int32_t portConfigId = portConfig.getId();
+            auto configIt = std::find_if(
+                    retrievedPortConfigs.begin(), retrievedPortConfigs.end(),
+                    [&portConfigId](const auto& retrConf) { return retrConf.id == portConfigId; });
+            EXPECT_NE(configIt, retrievedPortConfigs.end())
+                    << "Port config id returned by setAudioPortConfig: " << portConfigId
+                    << " is not found in the list returned by getAudioPortConfigs";
+            if (configIt != retrievedPortConfigs.end()) {
+                EXPECT_EQ(portConfig.get(), *configIt)
+                        << "Applied port config returned by setAudioPortConfig: "
+                        << portConfig.get().toString()
+                        << " is not the same as retrieved via getAudioPortConfigs: "
+                        << configIt->toString();
+            }
+        }
+    }
+
+    template <typename Entity>
+    void GetAllEntityIds(std::set<int32_t>* entityIds,
+                         Status (IModule::*getter)(std::vector<Entity>*),
+                         const std::string& errorMessage) {
+        std::vector<Entity> entities;
+        {
+            Status status = (module.get()->*getter)(&entities);
+            ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status;
+        }
+        std::transform(entities.begin(), entities.end(),
+                       std::inserter(*entityIds, entityIds->begin()),
+                       [](const auto& entity) { return entity.id; });
+        EXPECT_EQ(entities.size(), entityIds->size()) << errorMessage;
+    }
+
+    void GetAllPatchIds(std::set<int32_t>* patchIds) {
+        return GetAllEntityIds<AudioPatch>(
+                patchIds, &IModule::getAudioPatches,
+                "IDs of audio patches returned by IModule.getAudioPatches are not unique");
+    }
+
+    void GetAllPortIds(std::set<int32_t>* portIds) {
+        return GetAllEntityIds<AudioPort>(
+                portIds, &IModule::getAudioPorts,
+                "IDs of audio ports returned by IModule.getAudioPorts are not unique");
+    }
+
+    void GetAllPortConfigIds(std::set<int32_t>* portConfigIds) {
+        return GetAllEntityIds<AudioPortConfig>(
+                portConfigIds, &IModule::getAudioPortConfigs,
+                "IDs of audio port configs returned by IModule.getAudioPortConfigs are not unique");
+    }
+
+    void SetUpModuleConfig() {
+        if (moduleConfig == nullptr) {
+            moduleConfig = std::make_unique<ModuleConfig>(module.get());
+            ASSERT_EQ(Status::EX_NONE, moduleConfig->getStatus().exceptionCode())
+                    << "ModuleConfig init error: " << moduleConfig->getError();
+        }
+    }
+
+    sp<IModule> module;
+    sp<AidlDeathRecipient> deathHandler;
+    std::unique_ptr<ModuleConfig> moduleConfig;
+    WithDebugFlags debug;
+};
+
+class WithDevicePortConnectedState {
+  public:
+    explicit WithDevicePortConnectedState(const AudioPort& idAndData) : mIdAndData(idAndData) {}
+    WithDevicePortConnectedState(const AudioPort& id, const AudioDeviceAddress& address)
+        : mIdAndData(setAudioPortAddress(id, address)) {}
+    ~WithDevicePortConnectedState() {
+        if (mModule != nullptr) {
+            Status status = mModule->disconnectExternalDevice(getId());
+            EXPECT_EQ(Status::EX_NONE, status.exceptionCode())
+                    << status << " returned when disconnecting device port ID " << getId();
+        }
+    }
+    void SetUp(IModule* module) {
+        Status status = module->connectExternalDevice(mIdAndData, &mConnectedPort);
+        ASSERT_EQ(Status::EX_NONE, status.exceptionCode())
+                << status << " returned when connecting device port ID & data "
+                << mIdAndData.toString();
+        ASSERT_NE(mIdAndData.id, getId())
+                << "ID of the connected port must not be the same as the ID of the template port";
+        mModule = module;
+    }
+    int32_t getId() const { return mConnectedPort.id; }
+    const AudioPort& get() { return mConnectedPort; }
+
+  private:
+    static AudioPort setAudioPortAddress(const AudioPort& id, const AudioDeviceAddress& address) {
+        AudioPort result = id;
+        result.ext.get<AudioPortExt::Tag::device>().device.address = address;
+        return result;
+    }
+
+    const AudioPort mIdAndData;
+    IModule* mModule = nullptr;
+    AudioPort mConnectedPort;
+};
+
 template <typename Stream>
 class WithStream {
   public:
@@ -332,7 +443,7 @@
     ASSERT_NO_FATAL_FAILURE(GetAllPortIds(&portIds));
 }
 
-TEST_P(AudioCoreModule, GetAudioPortsIsStatic) {
+TEST_P(AudioCoreModule, GetAudioPortsIsStable) {
     std::vector<AudioPort> ports1;
     {
         Status status = module->getAudioPorts(&ports1);
@@ -344,13 +455,13 @@
         ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status;
     }
     ASSERT_EQ(ports1.size(), ports2.size())
-            << "Sizes of audio port arrays do not match across calls to getAudioPorts";
+            << "Sizes of audio port arrays do not match across consequent calls to getAudioPorts";
     std::sort(ports1.begin(), ports1.end());
     std::sort(ports2.begin(), ports2.end());
     EXPECT_EQ(ports1, ports2);
 }
 
-TEST_P(AudioCoreModule, GetAudioRoutesIsStatic) {
+TEST_P(AudioCoreModule, GetAudioRoutesIsStable) {
     std::vector<AudioRoute> routes1;
     {
         Status status = module->getAudioRoutes(&routes1);
@@ -362,7 +473,7 @@
         ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status;
     }
     ASSERT_EQ(routes1.size(), routes2.size())
-            << "Sizes of audio route arrays do not match across calls to getAudioRoutes";
+            << "Sizes of audio route arrays do not match across consequent calls to getAudioRoutes";
     std::sort(routes1.begin(), routes1.end());
     std::sort(routes2.begin(), routes2.end());
     EXPECT_EQ(routes1, routes2);
@@ -401,6 +512,32 @@
     }
 }
 
+TEST_P(AudioCoreModule, GetAudioRoutesForAudioPort) {
+    std::set<int32_t> portIds;
+    ASSERT_NO_FATAL_FAILURE(GetAllPortIds(&portIds));
+    if (portIds.empty()) {
+        GTEST_SKIP() << "No ports in the module.";
+    }
+    for (const auto portId : portIds) {
+        std::vector<AudioRoute> routes;
+        Status status = module->getAudioRoutesForAudioPort(portId, &routes);
+        EXPECT_EQ(Status::EX_NONE, status.exceptionCode()) << status;
+        for (const auto& r : routes) {
+            if (r.sinkPortId != portId) {
+                const auto& srcs = r.sourcePortIds;
+                EXPECT_TRUE(std::find(srcs.begin(), srcs.end(), portId) != srcs.end())
+                        << " port ID " << portId << " does not used by the route " << r.toString();
+            }
+        }
+    }
+    for (const auto portId : GetNonExistentIds(portIds)) {
+        std::vector<AudioRoute> routes;
+        Status status = module->getAudioRoutesForAudioPort(portId, &routes);
+        EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode())
+                << status << " returned for port ID " << portId;
+    }
+}
+
 TEST_P(AudioCoreModule, CheckDevicePorts) {
     std::vector<AudioPort> ports;
     {
@@ -486,7 +623,7 @@
         EXPECT_EQ(Status::EX_NONE, status.exceptionCode()) << status;
         EXPECT_EQ(portId, port.id);
     }
-    for (const auto portId : getNonExistentIds(portIds)) {
+    for (const auto portId : GetNonExistentIds(portIds)) {
         AudioPort port;
         Status status = module->getAudioPort(portId, &port);
         EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode())
@@ -494,10 +631,59 @@
     }
 }
 
+// Verify that HAL module reports for a connected device port at least one non-dynamic profile,
+// that is, a profile with actual supported configuration.
+// Note: This test relies on simulation of external device connections by the HAL module.
+TEST_P(AudioCoreModule, GetAudioPortWithExternalDevices) {
+    ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig());
+    std::vector<AudioPort> ports = moduleConfig->getExternalDevicePorts();
+    if (ports.empty()) {
+        GTEST_SKIP() << "No external devices in the module.";
+    }
+    for (const auto& port : ports) {
+        AudioPort portWithData = port;
+        portWithData.ext.get<AudioPortExt::Tag::device>().device.address =
+                GenerateUniqueDeviceAddress();
+        WithDevicePortConnectedState portConnected(portWithData);
+        ASSERT_NO_FATAL_FAILURE(portConnected.SetUp(module.get()));
+        const int32_t connectedPortId = portConnected.getId();
+        ASSERT_NE(portWithData.id, connectedPortId);
+        ASSERT_EQ(portWithData.ext.getTag(), portConnected.get().ext.getTag());
+        EXPECT_EQ(portWithData.ext.get<AudioPortExt::Tag::device>().device,
+                  portConnected.get().ext.get<AudioPortExt::Tag::device>().device);
+        // Verify that 'getAudioPort' and 'getAudioPorts' return the same connected port.
+        AudioPort connectedPort;
+        Status status = module->getAudioPort(connectedPortId, &connectedPort);
+        EXPECT_EQ(Status::EX_NONE, status.exceptionCode())
+                << status << " returned for getAudioPort port ID " << connectedPortId;
+        EXPECT_EQ(portConnected.get(), connectedPort);
+        const auto& portProfiles = connectedPort.profiles;
+        EXPECT_NE(0, portProfiles.size())
+                << "Connected port has no profiles: " << connectedPort.toString();
+        const auto dynamicProfileIt =
+                std::find_if(portProfiles.begin(), portProfiles.end(), [](const auto& profile) {
+                    return profile.format.type == AudioFormatType::DEFAULT;
+                });
+        EXPECT_EQ(portProfiles.end(), dynamicProfileIt) << "Connected port contains dynamic "
+                                                        << "profiles: " << connectedPort.toString();
+
+        std::vector<AudioPort> allPorts;
+        {
+            Status status = module->getAudioPorts(&allPorts);
+            ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status;
+        }
+        const auto allPortsIt = findById(allPorts, connectedPortId);
+        EXPECT_NE(allPorts.end(), allPortsIt);
+        if (allPortsIt != allPorts.end()) {
+            EXPECT_EQ(portConnected.get(), *allPortsIt);
+        }
+    }
+}
+
 TEST_P(AudioCoreModule, OpenStreamInvalidPortConfigId) {
     std::set<int32_t> portConfigIds;
     ASSERT_NO_FATAL_FAILURE(GetAllPortConfigIds(&portConfigIds));
-    for (const auto portConfigId : getNonExistentIds(portConfigIds)) {
+    for (const auto portConfigId : GetNonExistentIds(portConfigIds)) {
         {
             sp<IStreamIn> stream;
             Status status = module->openInputStream(portConfigId, {}, &stream);
@@ -537,7 +723,7 @@
 TEST_P(AudioCoreModule, ResetAudioPortConfigInvalidId) {
     std::set<int32_t> portConfigIds;
     ASSERT_NO_FATAL_FAILURE(GetAllPortConfigIds(&portConfigIds));
-    for (const auto portConfigId : getNonExistentIds(portConfigIds)) {
+    for (const auto portConfigId : GetNonExistentIds(portConfigIds)) {
         Status status = module->resetAudioPortConfig(portConfigId);
         EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode())
                 << status << " returned for port config ID " << portConfigId;
@@ -608,39 +794,35 @@
     EXPECT_EQ(suggestedConfig.flags.value(), appliedConfig.flags.value());
 }
 
+TEST_P(AudioCoreModule, SetAllAttachedDevicePortConfigs) {
+    ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig());
+    ASSERT_NO_FATAL_FAILURE(ApplyEveryConfig(moduleConfig->getPortConfigsForAttachedDevicePorts()));
+}
+
+// Note: This test relies on simulation of external device connections by the HAL module.
+TEST_P(AudioCoreModule, SetAllExternalDevicePortConfigs) {
+    ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig());
+    std::vector<AudioPort> ports = moduleConfig->getExternalDevicePorts();
+    if (ports.empty()) {
+        GTEST_SKIP() << "No external devices in the module.";
+    }
+    for (const auto& port : ports) {
+        WithDevicePortConnectedState portConnected(port, GenerateUniqueDeviceAddress());
+        ASSERT_NO_FATAL_FAILURE(portConnected.SetUp(module.get()));
+        ASSERT_NO_FATAL_FAILURE(
+                ApplyEveryConfig(moduleConfig->getPortConfigsForDevicePort(portConnected.get())));
+    }
+}
+
 TEST_P(AudioCoreModule, SetAllStaticAudioPortConfigs) {
     ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig());
-    const auto allPortConfigs = moduleConfig->getPortConfigsForMixPorts();
-    for (const auto& config : allPortConfigs) {
-        ASSERT_NE(0, config.portId);
-        WithAudioPortConfig portConfig(config);
-        ASSERT_NO_FATAL_FAILURE(portConfig.SetUp(module.get()));
-        EXPECT_EQ(config.portId, portConfig.get().portId);
-        std::vector<AudioPortConfig> retrievedPortConfigs;
-        {
-            Status status = module->getAudioPortConfigs(&retrievedPortConfigs);
-            ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status;
-        }
-        const int32_t portConfigId = portConfig.getId();
-        auto configIt = std::find_if(
-                retrievedPortConfigs.begin(), retrievedPortConfigs.end(),
-                [&portConfigId](const auto& retrConf) { return retrConf.id == portConfigId; });
-        EXPECT_NE(configIt, retrievedPortConfigs.end())
-                << "Port config id returned by setAudioPortConfig: " << portConfigId
-                << " is not found in the list returned by getPortConfigsForMixPorts";
-        if (configIt != retrievedPortConfigs.end()) {
-            EXPECT_EQ(portConfig.get(), *configIt)
-                    << "Port config returned by getPortConfigsForMixPorts: " << configIt->toString()
-                    << " is not the same as returned by setAudioPortConfig: "
-                    << portConfig.get().toString();
-        }
-    }
+    ASSERT_NO_FATAL_FAILURE(ApplyEveryConfig(moduleConfig->getPortConfigsForMixPorts()));
 }
 
 TEST_P(AudioCoreModule, SetAudioPortConfigInvalidPortId) {
     std::set<int32_t> portIds;
     ASSERT_NO_FATAL_FAILURE(GetAllPortIds(&portIds));
-    for (const auto portId : getNonExistentIds(portIds)) {
+    for (const auto portId : GetNonExistentIds(portIds)) {
         AudioPortConfig portConfig, suggestedConfig;
         bool applied;
         portConfig.portId = portId;
@@ -656,7 +838,7 @@
 TEST_P(AudioCoreModule, SetAudioPortConfigInvalidPortConfigId) {
     std::set<int32_t> portConfigIds;
     ASSERT_NO_FATAL_FAILURE(GetAllPortConfigIds(&portConfigIds));
-    for (const auto portConfigId : getNonExistentIds(portConfigIds)) {
+    for (const auto portConfigId : GetNonExistentIds(portConfigIds)) {
         AudioPortConfig portConfig, suggestedConfig;
         bool applied;
         portConfig.id = portConfigId;
@@ -669,6 +851,203 @@
     }
 }
 
+TEST_P(AudioCoreModule, TryConnectMissingDevice) {
+    ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig());
+    std::vector<AudioPort> ports = moduleConfig->getExternalDevicePorts();
+    if (ports.empty()) {
+        GTEST_SKIP() << "No external devices in the module.";
+    }
+    AudioPort ignored;
+    WithDebugFlags doNotSimulateConnections(debug);
+    doNotSimulateConnections.flags().simulateDeviceConnections = false;
+    ASSERT_NO_FATAL_FAILURE(doNotSimulateConnections.SetUp(module.get()));
+    for (const auto& port : ports) {
+        AudioPort portWithData = port;
+        portWithData.ext.get<AudioPortExt::Tag::device>().device.address =
+                GenerateUniqueDeviceAddress();
+        Status status = module->connectExternalDevice(portWithData, &ignored);
+        EXPECT_EQ(Status::EX_ILLEGAL_STATE, status.exceptionCode())
+                << status << " returned for static port " << portWithData.toString();
+    }
+}
+
+TEST_P(AudioCoreModule, TryChangingConnectionSimulationMidway) {
+    ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig());
+    std::vector<AudioPort> ports = moduleConfig->getExternalDevicePorts();
+    if (ports.empty()) {
+        GTEST_SKIP() << "No external devices in the module.";
+    }
+    WithDevicePortConnectedState portConnected(*ports.begin(), GenerateUniqueDeviceAddress());
+    ASSERT_NO_FATAL_FAILURE(portConnected.SetUp(module.get()));
+    ModuleDebug midwayDebugChange = debug.flags();
+    midwayDebugChange.simulateDeviceConnections = false;
+    Status status = module->setModuleDebug(midwayDebugChange);
+    EXPECT_EQ(Status::EX_ILLEGAL_STATE, status.exceptionCode())
+            << status << " returned when trying to disable connections simulation "
+            << "while having a connected device";
+}
+
+TEST_P(AudioCoreModule, ConnectDisconnectExternalDeviceInvalidPorts) {
+    AudioPort ignored;
+    std::set<int32_t> portIds;
+    ASSERT_NO_FATAL_FAILURE(GetAllPortIds(&portIds));
+    for (const auto portId : GetNonExistentIds(portIds)) {
+        AudioPort invalidPort;
+        invalidPort.id = portId;
+        Status status = module->connectExternalDevice(invalidPort, &ignored);
+        EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode())
+                << status << " returned for port ID " << portId << " when setting CONNECTED state";
+        status = module->disconnectExternalDevice(portId);
+        EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode())
+                << status << " returned for port ID " << portId
+                << " when setting DISCONNECTED state";
+    }
+
+    std::vector<AudioPort> ports;
+    {
+        Status status = module->getAudioPorts(&ports);
+        ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status;
+    }
+    for (const auto& port : ports) {
+        if (port.ext.getTag() != AudioPortExt::Tag::device) {
+            Status status = module->connectExternalDevice(port, &ignored);
+            EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode())
+                    << status << " returned for non-device port ID " << port.id
+                    << " when setting CONNECTED state";
+            status = module->disconnectExternalDevice(port.id);
+            EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode())
+                    << status << " returned for non-device port ID " << port.id
+                    << " when setting DISCONNECTED state";
+        } else {
+            const auto& devicePort = port.ext.get<AudioPortExt::Tag::device>();
+            if (devicePort.device.type.connection.empty()) {
+                Status status = module->connectExternalDevice(port, &ignored);
+                EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode())
+                        << status << " returned for permanently attached device port ID " << port.id
+                        << " when setting CONNECTED state";
+                status = module->disconnectExternalDevice(port.id);
+                EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode())
+                        << status << " returned for permanently attached device port ID " << port.id
+                        << " when setting DISCONNECTED state";
+            }
+        }
+    }
+}
+
+// Note: This test relies on simulation of external device connections by the HAL module.
+TEST_P(AudioCoreModule, ConnectDisconnectExternalDeviceTwice) {
+    ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig());
+    AudioPort ignored;
+    std::vector<AudioPort> ports = moduleConfig->getExternalDevicePorts();
+    if (ports.empty()) {
+        GTEST_SKIP() << "No external devices in the module.";
+    }
+    for (const auto& port : ports) {
+        Status status = module->disconnectExternalDevice(port.id);
+        EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode())
+                << status << " returned when disconnecting already disconnected device port ID "
+                << port.id;
+        AudioPort portWithData = port;
+        portWithData.ext.get<AudioPortExt::Tag::device>().device.address =
+                GenerateUniqueDeviceAddress();
+        WithDevicePortConnectedState portConnected(portWithData);
+        ASSERT_NO_FATAL_FAILURE(portConnected.SetUp(module.get()));
+        status = module->connectExternalDevice(portConnected.get(), &ignored);
+        EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode())
+                << status << " returned when trying to connect a connected device port "
+                << portConnected.get().toString();
+        status = module->connectExternalDevice(portWithData, &ignored);
+        EXPECT_EQ(Status::EX_ILLEGAL_STATE, status.exceptionCode())
+                << status << " returned when connecting again the external device "
+                << portWithData.ext.get<AudioPortExt::Tag::device>().device.toString();
+        if (status.exceptionCode() == Status::EX_NONE) {
+            ADD_FAILURE() << "Returned connected port " << ignored.toString() << " for template "
+                          << portWithData.toString();
+        }
+    }
+}
+
+// Note: This test relies on simulation of external device connections by the HAL module.
+TEST_P(AudioCoreModule, DisconnectExternalDeviceNonResetPortConfig) {
+    ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig());
+    std::vector<AudioPort> ports = moduleConfig->getExternalDevicePorts();
+    if (ports.empty()) {
+        GTEST_SKIP() << "No external devices in the module.";
+    }
+    for (const auto& port : ports) {
+        WithDevicePortConnectedState portConnected(port, GenerateUniqueDeviceAddress());
+        ASSERT_NO_FATAL_FAILURE(portConnected.SetUp(module.get()));
+        const auto portConfig = moduleConfig->getSingleConfigForDevicePort(portConnected.get());
+        {
+            WithAudioPortConfig config(portConfig);
+            // Note: if SetUp fails, check the status of 'GetAudioPortWithExternalDevices' test.
+            // Our test assumes that 'getAudioPort' returns at least one profile, and it
+            // is not a dynamic profile.
+            ASSERT_NO_FATAL_FAILURE(config.SetUp(module.get()));
+            Status status = module->disconnectExternalDevice(portConnected.getId());
+            EXPECT_EQ(Status::EX_ILLEGAL_STATE, status.exceptionCode())
+                    << status << " returned when trying to disconnect device port ID " << port.id
+                    << " with active configuration " << config.getId();
+        }
+    }
+}
+
+TEST_P(AudioCoreModule, ExternalDevicePortRoutes) {
+    ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig());
+    std::vector<AudioPort> ports = moduleConfig->getExternalDevicePorts();
+    if (ports.empty()) {
+        GTEST_SKIP() << "No external devices in the module.";
+    }
+    for (const auto& port : ports) {
+        std::vector<AudioRoute> routesBefore;
+        {
+            Status status = module->getAudioRoutes(&routesBefore);
+            ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status;
+        }
+
+        int32_t connectedPortId;
+        {
+            WithDevicePortConnectedState portConnected(port, GenerateUniqueDeviceAddress());
+            ASSERT_NO_FATAL_FAILURE(portConnected.SetUp(module.get()));
+            connectedPortId = portConnected.getId();
+            std::vector<AudioRoute> connectedPortRoutes;
+            {
+                Status status =
+                        module->getAudioRoutesForAudioPort(connectedPortId, &connectedPortRoutes);
+                ASSERT_EQ(Status::EX_NONE, status.exceptionCode())
+                        << status << " returned when retrieving routes for connected port id "
+                        << connectedPortId;
+            }
+            // There must be routes for the port to be useful.
+            if (connectedPortRoutes.empty()) {
+                std::vector<AudioRoute> allRoutes;
+                Status status = module->getAudioRoutes(&allRoutes);
+                ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status;
+                ADD_FAILURE() << " no routes returned for the connected port "
+                              << portConnected.get().toString()
+                              << "; all routes: " << android::internal::ToString(allRoutes);
+            }
+        }
+        std::vector<AudioRoute> ignored;
+        Status status = module->getAudioRoutesForAudioPort(connectedPortId, &ignored);
+        ASSERT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode())
+                << status << " returned when retrieving routes for released connected port id "
+                << connectedPortId;
+
+        std::vector<AudioRoute> routesAfter;
+        {
+            Status status = module->getAudioRoutes(&routesAfter);
+            ASSERT_EQ(Status::EX_NONE, status.exceptionCode()) << status;
+        }
+        ASSERT_EQ(routesBefore.size(), routesAfter.size())
+                << "Sizes of audio route arrays do not match after creating and "
+                << "releasing a connected port";
+        std::sort(routesBefore.begin(), routesBefore.end());
+        std::sort(routesAfter.begin(), routesAfter.end());
+        EXPECT_EQ(routesBefore, routesAfter);
+    }
+}
+
 template <typename Stream>
 class AudioStream : public AudioCoreModule {
   public:
@@ -899,7 +1278,7 @@
 
         std::set<int32_t> portConfigIds;
         ASSERT_NO_FATAL_FAILURE(GetAllPortConfigIds(&portConfigIds));
-        for (const auto portConfigId : getNonExistentIds(portConfigIds)) {
+        for (const auto portConfigId : GetNonExistentIds(portConfigIds)) {
             EXPECT_NO_FATAL_FAILURE(SetInvalidPatchHelper(
                     Status::EX_ILLEGAL_ARGUMENT, {portConfigId}, {sinkPortConfig.getId()}));
             EXPECT_NO_FATAL_FAILURE(SetInvalidPatchHelper(Status::EX_ILLEGAL_ARGUMENT,
@@ -969,7 +1348,7 @@
         // Then use the same patch setting, except for having an invalid ID.
         std::set<int32_t> patchIds;
         ASSERT_NO_FATAL_FAILURE(GetAllPatchIds(&patchIds));
-        for (const auto patchId : getNonExistentIds(patchIds)) {
+        for (const auto patchId : GetNonExistentIds(patchIds)) {
             AudioPatch patchWithNonExistendId = patch.get();
             patchWithNonExistendId.id = patchId;
             Status status = module->setAudioPatch(patchWithNonExistendId, &patchWithNonExistendId);
@@ -995,7 +1374,7 @@
 TEST_P(AudioModulePatch, ResetInvalidPatchId) {
     std::set<int32_t> patchIds;
     ASSERT_NO_FATAL_FAILURE(GetAllPatchIds(&patchIds));
-    for (const auto patchId : getNonExistentIds(patchIds)) {
+    for (const auto patchId : GetNonExistentIds(patchIds)) {
         Status status = module->resetAudioPatch(patchId);
         EXPECT_EQ(Status::EX_ILLEGAL_ARGUMENT, status.exceptionCode())
                 << status << " returned for patch ID " << patchId;