audio: Clarify profiles management for external devices

Clarify what should happen to mix port profiles after
connection of an external device. Add a test to verify
this behavior.

Also, add an XML file for the test runner for
VtsHalAudioCoreTargetTest.

Bug: 273252382
Test: atest VtsHalAudioCoreTargetTest
Change-Id: I3381dd29c5922bf31fa3a8ae6fa273597e8333a1
This commit is contained in:
Mikhail Naganov 2023-03-24 18:29:14 -07:00
parent 04b2cdba73
commit fe09942d2a
6 changed files with 134 additions and 19 deletions

View file

@ -192,6 +192,19 @@ interface IModule {
* device address is specified for a point-to-multipoint external device
* connection.
*
* Since not all modules have a DSP that could perform sample rate and
* format conversions, behavior related to mix port configurations may vary.
* For modules with a DSP, mix ports can be pre-configured and have a fixed
* set of audio profiles supported by the DSP. For modules without a DSP,
* audio profiles of mix ports may change after connecting an external
* device. The typical case is that the mix port has an empty set of
* profiles when no external devices are connected, and after external
* device connection it receives the same set of profiles as the device
* ports that they can be routed to. The client will re-query current port
* configurations using 'getAudioPorts'. All mix ports that can be routed to
* the connected device port must have a non-empty set of audio profiles
* after successful connection of an external device.
*
* 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'

View file

@ -456,38 +456,45 @@ ndk::ScopedAStatus Module::connectExternalDevice(const AudioPort& in_templateIdA
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);
for (auto connectedPortPair : mConnectedDevicePorts) {
auto connectedPortIt = findById<AudioPort>(ports, connectedPortPair.first);
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;
<< " is already connected at the device port id "
<< connectedPortPair.first;
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";
// TODO: Check the return value when it is ready for actual devices.
populateConnectedDevicePort(&connectedPort);
if (ndk::ScopedAStatus status = populateConnectedDevicePort(&connectedPort);
!status.isOk()) {
return status;
}
} else {
auto& connectedProfiles = getConfig().connectedProfiles;
if (auto connectedProfilesIt = connectedProfiles.find(templateId);
connectedProfilesIt != connectedProfiles.end()) {
connectedPort.profiles = connectedProfilesIt->second;
}
}
if (connectedPort.profiles.empty()) {
LOG(ERROR) << "Profiles of a connected port still empty after connecting external device "
<< connectedPort.toString();
return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
}
connectedPort.id = ++getConfig().nextPortId;
mConnectedDevicePorts.insert(connectedPort.id);
auto [connectedPortsIt, _] =
mConnectedDevicePorts.insert(std::pair(connectedPort.id, std::vector<int32_t>()));
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);
onExternalDeviceConnectionChanged(connectedPort, true /*connected*/);
*_aidl_return = std::move(connectedPort);
std::vector<int32_t> routablePortIds;
std::vector<AudioRoute> newRoutes;
auto& routes = getConfig().routes;
for (auto& r : routes) {
@ -497,15 +504,30 @@ ndk::ScopedAStatus Module::connectExternalDevice(const AudioPort& in_templateIdA
newRoute.sinkPortId = connectedPort.id;
newRoute.isExclusive = r.isExclusive;
newRoutes.push_back(std::move(newRoute));
routablePortIds.insert(routablePortIds.end(), r.sourcePortIds.begin(),
r.sourcePortIds.end());
} 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);
}
}
}
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() && portsIt->profiles.empty()) {
portsIt->profiles = connectedPort.profiles;
connectedPortsIt->second.push_back(portsIt->id);
}
}
*_aidl_return = std::move(connectedPort);
return ndk::ScopedAStatus::ok();
}
@ -520,7 +542,8 @@ ndk::ScopedAStatus Module::disconnectExternalDevice(int32_t in_portId) {
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) {
auto connectedPortsIt = mConnectedDevicePorts.find(in_portId);
if (connectedPortsIt == mConnectedDevicePorts.end()) {
LOG(ERROR) << __func__ << ": port id " << in_portId << " is not a connected device port";
return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
}
@ -541,7 +564,6 @@ ndk::ScopedAStatus Module::disconnectExternalDevice(int32_t in_portId) {
}
onExternalDeviceConnectionChanged(*portIt, false /*connected*/);
ports.erase(portIt);
mConnectedDevicePorts.erase(in_portId);
LOG(DEBUG) << __func__ << ": connected device port " << in_portId << " released";
auto& routes = getConfig().routes;
@ -556,6 +578,14 @@ ndk::ScopedAStatus Module::disconnectExternalDevice(int32_t in_portId) {
}
}
for (const auto mixPortId : connectedPortsIt->second) {
auto mixPortIt = findById<AudioPort>(ports, mixPortId);
if (mixPortIt != ports.end()) {
mixPortIt->profiles = {};
}
}
mConnectedDevicePorts.erase(connectedPortsIt);
return ndk::ScopedAStatus::ok();
}

View file

@ -33,7 +33,8 @@ struct Configuration {
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'.
// Port id -> List of profiles to use when the device port state is set to 'connected'
// in connection simulation mode.
std::map<int32_t, std::vector<::aidl::android::media::audio::common::AudioProfile>>
connectedProfiles;
std::vector<AudioRoute> routes;

View file

@ -177,8 +177,10 @@ class Module : public BnModule {
ChildInterface<IBluetooth> mBluetooth;
ChildInterface<IBluetoothA2dp> mBluetoothA2dp;
ChildInterface<IBluetoothLe> mBluetoothLe;
// ids of ports created at runtime via 'connectExternalDevice'.
std::set<int32_t> mConnectedDevicePorts;
// ids of device ports created at runtime via 'connectExternalDevice'.
// Also stores ids of mix ports with dynamic profiles which got populated from the connected
// port.
std::map<int32_t, std::vector<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.

View file

@ -1780,6 +1780,42 @@ TEST_P(AudioCoreModule, ExternalDevicePortRoutes) {
}
}
// Note: This test relies on simulation of external device connections by the HAL module.
TEST_P(AudioCoreModule, ExternalDeviceMixPortConfigs) {
// After an external device has been connected, all mix ports that can be routed
// to the device port for the connected device must have non-empty profiles.
ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig());
std::vector<AudioPort> externalDevicePorts = moduleConfig->getExternalDevicePorts();
if (externalDevicePorts.empty()) {
GTEST_SKIP() << "No external devices in the module.";
}
for (const auto& port : externalDevicePorts) {
WithDevicePortConnectedState portConnected(GenerateUniqueDeviceAddress(port));
ASSERT_NO_FATAL_FAILURE(portConnected.SetUp(module.get()));
std::vector<AudioRoute> routes;
ASSERT_IS_OK(module->getAudioRoutesForAudioPort(portConnected.getId(), &routes));
std::vector<AudioPort> allPorts;
ASSERT_IS_OK(module->getAudioPorts(&allPorts));
for (const auto& r : routes) {
if (r.sinkPortId == portConnected.getId()) {
for (const auto& srcPortId : r.sourcePortIds) {
const auto srcPortIt = findById(allPorts, srcPortId);
ASSERT_NE(allPorts.end(), srcPortIt) << "port ID " << srcPortId;
EXPECT_NE(0UL, srcPortIt->profiles.size())
<< " source port " << srcPortIt->toString() << " must have its profiles"
<< " populated following external device connection";
}
} else {
const auto sinkPortIt = findById(allPorts, r.sinkPortId);
ASSERT_NE(allPorts.end(), sinkPortIt) << "port ID " << r.sinkPortId;
EXPECT_NE(0UL, sinkPortIt->profiles.size())
<< " source port " << sinkPortIt->toString() << " must have its"
<< " profiles populated following external device connection";
}
}
}
}
TEST_P(AudioCoreModule, MasterMute) {
bool isSupported = false;
EXPECT_NO_FATAL_FAILURE(TestAccessors<bool>(module.get(), &IModule::getMasterMute,

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<configuration description="Runs VtsHalAudioCoreTargetTest.">
<option name="test-suite-tag" value="apct" />
<option name="test-suite-tag" value="apct-native" />
<target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
<target_preparer class="com.android.tradefed.targetprep.StopServicesSetup"/>
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="run-command" value="setprop vts.native_server.on 1"/>
<option name="teardown-command" value="setprop vts.native_server.on 0"/>
</target_preparer>
<test class="com.android.tradefed.testtype.GTest" >
<option name="native-test-device-path" value="/data/local/tmp" />
<option name="module-name" value="VtsHalAudioCoreTargetTest" />
<option name="native-test-timeout" value="10m" />
</test>
</configuration>