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:
parent
04b2cdba73
commit
fe09942d2a
6 changed files with 134 additions and 19 deletions
|
@ -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'
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
33
audio/aidl/vts/VtsHalAudioCoreTargetTest.xml
Normal file
33
audio/aidl/vts/VtsHalAudioCoreTargetTest.xml
Normal 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>
|
Loading…
Reference in a new issue