Merge changes from topic "fix-b-273252382-connect-external-device"

* changes:
  audio: Clarify profiles management for external devices
  audio: Add some utility methods, improve logging
This commit is contained in:
Shunkai Yao 2023-03-31 05:35:08 +00:00 committed by Gerrit Code Review
commit ddd489824d
7 changed files with 163 additions and 24 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

@ -99,6 +99,12 @@ constexpr size_t getFrameSizeInBytes(
return 0;
}
constexpr bool isDefaultAudioFormat(
const ::aidl::android::media::audio::common::AudioFormatDescription& desc) {
return desc.type == ::aidl::android::media::audio::common::AudioFormatType::DEFAULT &&
desc.pcm == ::aidl::android::media::audio::common::PcmType::DEFAULT;
}
constexpr bool isTelephonyDeviceType(
::aidl::android::media::audio::common::AudioDeviceType device) {
return device == ::aidl::android::media::audio::common::AudioDeviceType::IN_TELEPHONY_RX ||

View file

@ -143,6 +143,21 @@ StreamOut::CreateInstance Module::getStreamOutCreator(Type type) {
}
}
std::ostream& operator<<(std::ostream& os, Module::Type t) {
switch (t) {
case Module::Type::DEFAULT:
os << "default";
break;
case Module::Type::R_SUBMIX:
os << "r_submix";
break;
case Module::Type::USB:
os << "usb";
break;
}
return os;
}
void Module::cleanUpPatch(int32_t patchId) {
erase_all_values(mPatches, std::set<int32_t>{patchId});
}
@ -352,16 +367,17 @@ void Module::updateStreamsConnectedState(const AudioPatch& oldPatch, const Audio
ndk::ScopedAStatus Module::setModuleDebug(
const ::aidl::android::hardware::audio::core::ModuleDebug& in_debug) {
LOG(DEBUG) << __func__ << ": old flags:" << mDebug.toString()
LOG(DEBUG) << __func__ << ": " << mType << ": 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";
LOG(ERROR) << __func__ << ": " << mType
<< ": attempting to change device connections simulation while having external "
<< "devices connected";
return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
}
if (in_debug.streamTransientStateDelayMs < 0) {
LOG(ERROR) << __func__ << ": streamTransientStateDelayMs is negative: "
LOG(ERROR) << __func__ << ": " << mType << ": streamTransientStateDelayMs is negative: "
<< in_debug.streamTransientStateDelayMs;
return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
}
@ -440,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) {
@ -481,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();
}
@ -504,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);
}
@ -525,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;
@ -540,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

@ -129,7 +129,9 @@ AudioDeviceAddress::Tag suggestDeviceAddressTag(const AudioDeviceDescription& de
using Tag = AudioDeviceAddress::Tag;
if (std::string_view connection = description.connection;
connection == AudioDeviceDescription::CONNECTION_BT_A2DP ||
connection == AudioDeviceDescription::CONNECTION_BT_LE ||
// Note: BT LE Broadcast uses a "group id".
(description.type != AudioDeviceType::OUT_BROADCAST &&
connection == AudioDeviceDescription::CONNECTION_BT_LE) ||
connection == AudioDeviceDescription::CONNECTION_BT_SCO ||
connection == AudioDeviceDescription::CONNECTION_WIRELESS) {
return Tag::mac;
@ -1778,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>