audio: Allow "dynamic" profiles for device ports

Some device ports are connected via ADSP which takes care
of the actual audio configuration (format, channels, SR),
for example the built-in speaker and mic ports, as well
as some external devices like analog headsets.

In the legacy implementation, such device ports did not have
any profiles specified. Allow the same behavior in the AIDL
implementation. To ensure correctness, device ports with no
profiles must be routable to mix ports that have profiles
specified. This requirement is fulfilled in legacy configs.

Bug: 266124463
Test: atest VtsHalAudioCoreTargetTest
Test: atest audiosystem_tests audiorouting_tests
Test: atest CtsMediaAudioTestCases
Change-Id: Iaccd1e8ef2a5af9a5f8bae453905d01c6b7fdc28
diff --git a/audio/aidl/default/Module.cpp b/audio/aidl/default/Module.cpp
index 76132b3..d721b32 100644
--- a/audio/aidl/default/Module.cpp
+++ b/audio/aidl/default/Module.cpp
@@ -65,32 +65,56 @@
 
 namespace {
 
+inline bool hasDynamicChannelMasks(const std::vector<AudioChannelLayout>& channelMasks) {
+    return channelMasks.empty() ||
+           std::all_of(channelMasks.begin(), channelMasks.end(),
+                       [](const auto& channelMask) { return channelMask == AudioChannelLayout{}; });
+}
+
+inline bool hasDynamicFormat(const AudioFormatDescription& format) {
+    return format == AudioFormatDescription{};
+}
+
+inline bool hasDynamicSampleRates(const std::vector<int32_t>& sampleRates) {
+    return sampleRates.empty() ||
+           std::all_of(sampleRates.begin(), sampleRates.end(),
+                       [](const auto& sampleRate) { return sampleRate == 0; });
+}
+
+inline bool isDynamicProfile(const AudioProfile& profile) {
+    return hasDynamicFormat(profile.format) || hasDynamicChannelMasks(profile.channelMasks) ||
+           hasDynamicSampleRates(profile.sampleRates);
+}
+
+bool hasDynamicProfilesOnly(const std::vector<AudioProfile>& profiles) {
+    if (profiles.empty()) return true;
+    return std::all_of(profiles.begin(), profiles.end(), isDynamicProfile);
+}
+
+// Note: does not assign an ID to the config.
 bool generateDefaultPortConfig(const AudioPort& port, AudioPortConfig* config) {
+    const bool allowDynamicConfig = port.ext.getTag() == AudioPortExt::device;
     *config = {};
     config->portId = port.id;
-    if (port.profiles.empty()) {
-        LOG(ERROR) << __func__ << ": port " << port.id << " has no profiles";
-        return false;
+    for (const auto& profile : port.profiles) {
+        if (isDynamicProfile(profile)) continue;
+        config->format = profile.format;
+        config->channelMask = *profile.channelMasks.begin();
+        config->sampleRate = Int{.value = *profile.sampleRates.begin()};
+        config->flags = port.flags;
+        config->ext = port.ext;
+        return true;
     }
-    const auto& profile = port.profiles.begin();
-    config->format = profile->format;
-    if (profile->channelMasks.empty()) {
-        LOG(ERROR) << __func__ << ": the first profile in port " << port.id
-                   << " has no channel masks";
-        return false;
+    if (allowDynamicConfig) {
+        config->format = AudioFormatDescription{};
+        config->channelMask = AudioChannelLayout{};
+        config->sampleRate = Int{.value = 0};
+        config->flags = port.flags;
+        config->ext = port.ext;
+        return true;
     }
-    config->channelMask = *profile->channelMasks.begin();
-    if (profile->sampleRates.empty()) {
-        LOG(ERROR) << __func__ << ": the first profile in port " << port.id
-                   << " has no sample rates";
-        return false;
-    }
-    Int sampleRate;
-    sampleRate.value = *profile->sampleRates.begin();
-    config->sampleRate = sampleRate;
-    config->flags = port.flags;
-    config->ext = port.ext;
-    return true;
+    LOG(ERROR) << __func__ << ": port " << port.id << " only has dynamic profiles";
+    return false;
 }
 
 bool findAudioProfile(const AudioPort& port, const AudioFormatDescription& format,
@@ -314,6 +338,18 @@
     return config;
 }
 
+std::vector<AudioRoute*> Module::getAudioRoutesForAudioPortImpl(int32_t portId) {
+    std::vector<AudioRoute*> result;
+    auto& routes = getConfig().routes;
+    for (auto& r : routes) {
+        const auto& srcs = r.sourcePortIds;
+        if (r.sinkPortId == portId || std::find(srcs.begin(), srcs.end(), portId) != srcs.end()) {
+            result.push_back(&r);
+        }
+    }
+    return result;
+}
+
 internal::Configuration& Module::getConfig() {
     if (!mConfig) {
         mConfig = std::move(initializeConfig());
@@ -321,6 +357,24 @@
     return *mConfig;
 }
 
+std::set<int32_t> Module::getRoutableAudioPortIds(int32_t portId,
+                                                  std::vector<AudioRoute*>* routes) {
+    std::vector<AudioRoute*> routesStorage;
+    if (routes == nullptr) {
+        routesStorage = getAudioRoutesForAudioPortImpl(portId);
+        routes = &routesStorage;
+    }
+    std::set<int32_t> result;
+    for (AudioRoute* r : *routes) {
+        if (r->sinkPortId == portId) {
+            result.insert(r->sourcePortIds.begin(), r->sourcePortIds.end());
+        } else {
+            result.insert(r->sinkPortId);
+        }
+    }
+    return result;
+}
+
 void Module::registerPatch(const AudioPatch& patch) {
     auto& configs = getConfig().portConfigs;
     auto do_insert = [&](const std::vector<int32_t>& portConfigIds) {
@@ -510,7 +564,31 @@
         }
     }
 
-    if (connectedPort.profiles.empty()) {
+    // Two main cases are considered with regard to the profiles of the connected device port:
+    //
+    //  1. If the template device port has dynamic profiles, and at least one routable mix
+    //     port also has dynamic profiles, it means that after connecting the device, the
+    //     connected device port must have profiles populated with actual capabilities of
+    //     the connected device, and dynamic of routable mix ports will be filled
+    //     according to these capabilities. An example of this case is connection of an
+    //     HDMI or USB device. For USB handled by ADSP, there can be mix ports with static
+    //     profiles, and one dedicated mix port for "hi-fi" playback. The latter is left with
+    //     dynamic profiles so that they can be populated with actual capabilities of
+    //     the connected device.
+    //
+    //  2. If the template device port has dynamic profiles, while all routable mix ports
+    //     have static profiles, it means that after connecting the device, the connected
+    //     device port can be left with dynamic profiles, and profiles of mix ports are
+    //     left untouched. An example of this case is connection of an analog wired
+    //     headset, it should be treated in the same way as a speaker.
+    //
+    //  Yet another possible case is when both the template device port and all routable
+    //  mix ports have static profiles. This is allowed and handled correctly, however, it
+    //  is not very practical, since these profiles are likely duplicates of each other.
+
+    std::vector<AudioRoute*> routesToMixPorts = getAudioRoutesForAudioPortImpl(templateId);
+    std::set<int32_t> routableMixPortIds = getRoutableAudioPortIds(templateId, &routesToMixPorts);
+    if (hasDynamicProfilesOnly(connectedPort.profiles)) {
         if (!mDebug.simulateDeviceConnections) {
             RETURN_STATUS_IF_ERROR(populateConnectedDevicePort(&connectedPort));
         } else {
@@ -520,23 +598,22 @@
                 connectedPort.profiles = connectedProfilesIt->second;
             }
         }
-        if (connectedPort.profiles.empty()) {
-            LOG(ERROR) << __func__
-                       << ": profiles of a connected port still empty after connecting external "
-                          "device "
-                       << connectedPort.toString();
-            return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
-        }
-    }
-
-    for (auto profile : connectedPort.profiles) {
-        if (profile.channelMasks.empty()) {
-            LOG(ERROR) << __func__ << ": the profile " << profile.name << " has no channel masks";
-            return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
-        }
-        if (profile.sampleRates.empty()) {
-            LOG(ERROR) << __func__ << ": the profile " << profile.name << " has no sample rates";
-            return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+        if (hasDynamicProfilesOnly(connectedPort.profiles)) {
+            // Possible case 2. Check if all routable mix ports have static profiles.
+            if (auto dynamicMixPortIt = std::find_if(ports.begin(), ports.end(),
+                                                     [&routableMixPortIds](const auto& p) {
+                                                         return routableMixPortIds.count(p.id) >
+                                                                        0 &&
+                                                                hasDynamicProfilesOnly(p.profiles);
+                                                     });
+                dynamicMixPortIt != ports.end()) {
+                LOG(ERROR) << __func__
+                           << ": connected port only has dynamic profiles after connecting "
+                           << "external device " << connectedPort.toString() << ", and there exist "
+                           << "a routable mix port with dynamic profiles: "
+                           << dynamicMixPortIt->toString();
+                return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+            }
         }
     }
 
@@ -548,44 +625,36 @@
     ports.push_back(connectedPort);
     onExternalDeviceConnectionChanged(connectedPort, true /*connected*/);
 
-    std::vector<int32_t> routablePortIds;
+    // For routes where the template port is a source, add the connected port to sources,
+    // otherwise, create a new route by copying from the route for the template port.
     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));
-            routablePortIds.insert(routablePortIds.end(), r.sourcePortIds.begin(),
-                                   r.sourcePortIds.end());
+    for (AudioRoute* r : routesToMixPorts) {
+        if (r->sinkPortId == templateId) {
+            newRoutes.push_back(AudioRoute{.sourcePortIds = r->sourcePortIds,
+                                           .sinkPortId = connectedPort.id,
+                                           .isExclusive = r->isExclusive});
         } else {
-            auto& srcs = r.sourcePortIds;
-            if (std::find(srcs.begin(), srcs.end(), templateId) != srcs.end()) {
-                srcs.push_back(connectedPort.id);
-                routablePortIds.push_back(r.sinkPortId);
-            }
+            r->sourcePortIds.push_back(connectedPort.id);
         }
     }
+    auto& routes = getConfig().routes;
     routes.insert(routes.end(), newRoutes.begin(), newRoutes.end());
 
-    // Note: this is a simplistic approach assuming that a mix port can only be populated
-    // from a single device port. Implementing support for stuffing dynamic profiles with a superset
-    // of all profiles from all routable dynamic device ports would be more involved.
-    for (const auto mixPortId : routablePortIds) {
-        auto portsIt = findById<AudioPort>(ports, mixPortId);
-        if (portsIt != ports.end()) {
-            if (portsIt->profiles.empty()) {
-                portsIt->profiles = connectedPort.profiles;
-                connectedPortsIt->second.insert(portsIt->id);
+    if (!hasDynamicProfilesOnly(connectedPort.profiles) && !routableMixPortIds.empty()) {
+        // Note: this is a simplistic approach assuming that a mix port can only be populated
+        // from a single device port. Implementing support for stuffing dynamic profiles with
+        // a superset of all profiles from all routable dynamic device ports would be more involved.
+        for (auto& port : ports) {
+            if (routableMixPortIds.count(port.id) == 0) continue;
+            if (hasDynamicProfilesOnly(port.profiles)) {
+                port.profiles = connectedPort.profiles;
+                connectedPortsIt->second.insert(port.id);
             } else {
-                // Check if profiles are non empty because they were populated by
-                // a previous connection. Otherwise, it means that they are not empty because
-                // the mix port has static profiles.
-                for (const auto cp : mConnectedDevicePorts) {
-                    if (cp.second.count(portsIt->id) > 0) {
-                        connectedPortsIt->second.insert(portsIt->id);
+                // Check if profiles are not all dynamic because they were populated by
+                // a previous connection. Otherwise, it means that they are actually static.
+                for (const auto& cp : mConnectedDevicePorts) {
+                    if (cp.second.count(port.id) > 0) {
+                        connectedPortsIt->second.insert(port.id);
                         break;
                     }
                 }
@@ -705,13 +774,9 @@
         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();
-                 });
+    std::vector<AudioRoute*> routes = getAudioRoutesForAudioPortImpl(in_portId);
+    std::transform(routes.begin(), routes.end(), std::back_inserter(*_aidl_return),
+                   [](auto rptr) { return *rptr; });
     return ndk::ScopedAStatus::ok();
 }
 
@@ -925,13 +990,14 @@
 
     const int portId = existing != configs.end() ? existing->portId : in_requested.portId;
     if (portId == 0) {
-        LOG(ERROR) << __func__ << ": input port config does not specify portId";
+        LOG(ERROR) << __func__ << ": requested port config does not specify portId";
         return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
     }
     auto& ports = getConfig().ports;
     auto portIt = findById<AudioPort>(ports, portId);
     if (portIt == ports.end()) {
-        LOG(ERROR) << __func__ << ": input port config points to non-existent portId " << portId;
+        LOG(ERROR) << __func__ << ": requested port config points to non-existent portId "
+                   << portId;
         return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
     }
     if (existing != configs.end()) {
@@ -949,6 +1015,10 @@
     // or a new generated config. Now attempt to update it according to the specified
     // fields of 'in_requested'.
 
+    // Device ports with only dynamic profiles are used for devices that are connected via ADSP,
+    // which takes care of their actual configuration automatically.
+    const bool allowDynamicConfig = portIt->ext.getTag() == AudioPortExt::device &&
+                                    hasDynamicProfilesOnly(portIt->profiles);
     bool requestedIsValid = true, requestedIsFullySpecified = true;
 
     AudioIoFlags portFlags = portIt->flags;
@@ -966,17 +1036,19 @@
     AudioProfile portProfile;
     if (in_requested.format.has_value()) {
         const auto& format = in_requested.format.value();
-        if (findAudioProfile(*portIt, format, &portProfile)) {
+        if ((format == AudioFormatDescription{} && allowDynamicConfig) ||
+            findAudioProfile(*portIt, format, &portProfile)) {
             out_suggested->format = format;
         } else {
             LOG(WARNING) << __func__ << ": requested format " << format.toString()
-                         << " is not found in port's " << portId << " profiles";
+                         << " is not found in the profiles of port " << portId;
             requestedIsValid = false;
         }
     } else {
         requestedIsFullySpecified = false;
     }
-    if (!findAudioProfile(*portIt, out_suggested->format.value(), &portProfile)) {
+    if (!(out_suggested->format.value() == AudioFormatDescription{} && allowDynamicConfig) &&
+        !findAudioProfile(*portIt, out_suggested->format.value(), &portProfile)) {
         LOG(ERROR) << __func__ << ": port " << portId << " does not support format "
                    << out_suggested->format.value().toString() << " anymore";
         return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
@@ -984,8 +1056,9 @@
 
     if (in_requested.channelMask.has_value()) {
         const auto& channelMask = in_requested.channelMask.value();
-        if (find(portProfile.channelMasks.begin(), portProfile.channelMasks.end(), channelMask) !=
-            portProfile.channelMasks.end()) {
+        if ((channelMask == AudioChannelLayout{} && allowDynamicConfig) ||
+            find(portProfile.channelMasks.begin(), portProfile.channelMasks.end(), channelMask) !=
+                    portProfile.channelMasks.end()) {
             out_suggested->channelMask = channelMask;
         } else {
             LOG(WARNING) << __func__ << ": requested channel mask " << channelMask.toString()
@@ -999,7 +1072,8 @@
 
     if (in_requested.sampleRate.has_value()) {
         const auto& sampleRate = in_requested.sampleRate.value();
-        if (find(portProfile.sampleRates.begin(), portProfile.sampleRates.end(),
+        if ((sampleRate.value == 0 && allowDynamicConfig) ||
+            find(portProfile.sampleRates.begin(), portProfile.sampleRates.end(),
                  sampleRate.value) != portProfile.sampleRates.end()) {
             out_suggested->sampleRate = sampleRate;
         } else {
@@ -1397,7 +1471,18 @@
     return mIsMmapSupported.value();
 }
 
-ndk::ScopedAStatus Module::populateConnectedDevicePort(AudioPort* audioPort __unused) {
+ndk::ScopedAStatus Module::populateConnectedDevicePort(AudioPort* audioPort) {
+    if (audioPort->ext.getTag() != AudioPortExt::device) {
+        LOG(ERROR) << __func__ << ": not a device port: " << audioPort->toString();
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+    }
+    const auto& devicePort = audioPort->ext.get<AudioPortExt::device>();
+    if (!devicePort.device.type.connection.empty()) {
+        LOG(ERROR) << __func__
+                   << ": module implementation must override 'populateConnectedDevicePort' "
+                   << "to handle connection of external devices.";
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+    }
     LOG(VERBOSE) << __func__ << ": do nothing and return ok";
     return ndk::ScopedAStatus::ok();
 }