aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrzegorz Kołodziejczyk <grzegorz.kolodziejczyk@codecoup.pl>2021-10-06 11:54:53 +0000
committerŁukasz Rymanowski <lukasz.rymanowski@codecoup.pl>2021-10-14 16:18:27 +0000
commit280b895ce020356ec9862f24de3dacea99bdf813 (patch)
tree9827af18726ea1caa56d4ffa6b3f584c32c0cc12
parented675df608931d6469e91189b2bcd251970d1bff (diff)
downloadbt-280b895ce020356ec9862f24de3dacea99bdf813.tar.gz
leaudio: Add BAP and CSIP integration along with audio path
LE Audio implements PAC handling, device grouping into sets, audio policy by the content type, group stream management, broadcast etc. Devices are always part of the group which creates sets. Sets can be managed by the LeAudio system service which has an API to add/remove specific devices to/from the group. Any stream operations are done by the LeAudio system service with a usage of the group_id. Note, this group_id is not the same number as the CIG_ID however, we might say it has the same meaning. When devices are part of a coordinated set (CSIS), then LeAudio implementation will discover all of them and notify upper layer about them. Upper layers are to decide to bond CSIS devices. Native code will group them. Fluoride also integrates Android Audio 2.1 framework and once stream is established then Le Audio session is opened and ready to receive periodically (up to implementation) audio data from media server (16bit, 16kHz/24kHz/32kHz/48kHz PCM - up to policy implementation) from the audio system. PCM frames are encoded by LC3 codec (system/bt/embdrv/lc3) and pushed over HCI to the controller. Bug: 150670922 Tag: #feature Sponsor: jpawlowski@ Test: atest --host bluetooth_le_audio_test bluetooth_le_audio_client_test Change-Id: I25d9422fb2b43843a59a9e398fac1e09c7e4869c
-rw-r--r--binder/android/bluetooth/IBluetoothLeAudio.aidl1
-rw-r--r--bta/Android.bp117
-rw-r--r--bta/csis/csis_client.cc2
-rw-r--r--bta/include/bta_le_audio_api.h48
-rw-r--r--bta/le_audio/client.cc2839
-rw-r--r--bta/le_audio/client_audio.cc573
-rw-r--r--bta/le_audio/client_audio.h131
-rw-r--r--bta/le_audio/client_audio_test.cc519
-rw-r--r--bta/le_audio/client_parser.cc628
-rw-r--r--bta/le_audio/client_parser.h242
-rw-r--r--bta/le_audio/client_parser_test.cc1647
-rw-r--r--bta/le_audio/devices.cc1778
-rw-r--r--bta/le_audio/devices.h315
-rw-r--r--bta/le_audio/devices_test.cc898
-rw-r--r--bta/le_audio/le_audio_client_test.cc2644
-rw-r--r--bta/le_audio/le_audio_types.cc409
-rw-r--r--bta/le_audio/le_audio_types.h952
-rw-r--r--bta/le_audio/le_audio_types_test.cc162
-rw-r--r--bta/le_audio/mock_iso_manager.cc155
-rw-r--r--bta/le_audio/mock_iso_manager.h74
-rw-r--r--bta/le_audio/mock_le_audio_client_audio.cc135
-rw-r--r--bta/le_audio/mock_le_audio_client_audio.h53
-rw-r--r--bta/le_audio/mock_le_audio_client_audio_source.cc50
-rw-r--r--bta/le_audio/mock_le_audio_client_audio_source.h35
-rw-r--r--bta/le_audio/mock_state_machine.cc43
-rw-r--r--bta/le_audio/mock_state_machine.h90
-rw-r--r--bta/le_audio/state_machine.cc1832
-rw-r--r--bta/le_audio/state_machine.h87
-rw-r--r--bta/le_audio/state_machine_test.cc2108
-rw-r--r--bta/test/common/btif_storage_mock.cc39
-rw-r--r--bta/test/common/btif_storage_mock.h51
-rw-r--r--btif/Android.bp7
-rw-r--r--btif/include/btif_storage.h10
-rw-r--r--btif/src/bluetooth.cc2
-rw-r--r--btif/src/btif_dm.cc10
-rw-r--r--btif/src/btif_le_audio.cc67
-rw-r--r--btif/src/btif_leaudio_hal_version_host.cc30
-rw-r--r--btif/src/btif_storage.cc60
-rw-r--r--embdrv/lc3/Android.bp18
-rw-r--r--gd/rust/topshim/facade/Android.bp1
-rw-r--r--include/hardware/bluetooth.h1
-rw-r--r--main/Android.bp2
-rw-r--r--stack/test/fuzzers/Android.bp1
-rw-r--r--test/Android.bp8
-rw-r--r--test/mock/mock_bta_leaudio.cc67
45 files changed, 18921 insertions, 20 deletions
diff --git a/binder/android/bluetooth/IBluetoothLeAudio.aidl b/binder/android/bluetooth/IBluetoothLeAudio.aidl
index f8901ed20..2f158e3d3 100644
--- a/binder/android/bluetooth/IBluetoothLeAudio.aidl
+++ b/binder/android/bluetooth/IBluetoothLeAudio.aidl
@@ -46,6 +46,7 @@ interface IBluetoothLeAudio {
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
int getConnectionPolicy(in BluetoothDevice device, in AttributionSource attributionSource);
+ /* Same value as bluetooth::groups::kGroupUnknown */
const int LE_AUDIO_GROUP_ID_INVALID = -1;
const int GROUP_STATUS_INACTIVE = 0;
diff --git a/bta/Android.bp b/bta/Android.bp
index 83045ca3b..cbfdee5b8 100644
--- a/bta/Android.bp
+++ b/bta/Android.bp
@@ -91,6 +91,12 @@ cc_library_static {
"groups/groups.cc",
"vc/device.cc",
"vc/vc.cc",
+ "le_audio/client.cc",
+ "le_audio/devices.cc",
+ "le_audio/state_machine.cc",
+ "le_audio/client_parser.cc",
+ "le_audio/client_audio.cc",
+ "le_audio/le_audio_types.cc",
"hearing_aid/hearing_aid.cc",
"hearing_aid/hearing_aid_audio_source.cc",
"hf_client/bta_hf_client_act.cc",
@@ -394,3 +400,114 @@ cc_test {
cfi: false,
},
}
+
+// bta unit tests for LE Audio
+// ========================================================
+cc_test {
+ name: "bluetooth_le_audio_test",
+ test_suites: ["device-tests"],
+ defaults: [
+ "fluoride_defaults",
+ "clang_coverage_bin",
+ ],
+ host_supported: true,
+ include_dirs: [
+ "system/bt",
+ "system/bt/bta/include",
+ "system/bt/bta/test/common",
+ "system/bt/btif/include",
+ "system/bt/gd",
+ "system/bt/stack/include",
+ ],
+ srcs : [
+ ":TestStubOsi",
+ "test/common/bta_gatt_api_mock.cc",
+ "test/common/bta_gatt_queue_mock.cc",
+ "test/common/btm_api_mock.cc",
+ "le_audio/client_audio.cc",
+ "le_audio/client_audio_test.cc",
+ "le_audio/client_parser.cc",
+ "le_audio/client_parser_test.cc",
+ "le_audio/devices.cc",
+ "le_audio/devices_test.cc",
+ "le_audio/le_audio_types.cc",
+ "le_audio/le_audio_types_test.cc",
+ "le_audio/mock_iso_manager.cc",
+ "test/common/mock_controller.cc",
+ "le_audio/state_machine.cc",
+ "le_audio/state_machine_test.cc"
+ ],
+ shared_libs: [
+ "libprotobuf-cpp-lite",
+ "libcrypto",
+ "liblog", // __android_log_print
+ ],
+ static_libs : [
+ "libgmock",
+ "libbt-common",
+ "libbt-protos-lite",
+ "libosi",
+ ],
+ sanitize: {
+ cfi: false,
+ },
+}
+
+cc_test {
+ name: "bluetooth_le_audio_client_test",
+ test_suites: ["device-tests"],
+ defaults: [
+ "fluoride_bta_defaults",
+ "clang_coverage_bin",
+ ],
+ host_supported: true,
+ include_dirs: [
+ "system/bt",
+ "system/bt/bta/include",
+ "system/bt/bta/test/common",
+ "system/bt/stack/include",
+ ],
+ srcs : [
+ "gatt/database.cc",
+ "gatt/database_builder.cc",
+ "le_audio/client.cc",
+ "le_audio/client_parser.cc",
+ "le_audio/devices.cc",
+ "le_audio/le_audio_client_test.cc",
+ "le_audio/le_audio_types.cc",
+ "le_audio/mock_iso_manager.cc",
+ "le_audio/mock_le_audio_client_audio.cc",
+ "le_audio/mock_state_machine.cc",
+ "test/common/btm_api_mock.cc",
+ "test/common/bta_gatt_api_mock.cc",
+ "test/common/bta_gatt_queue_mock.cc",
+ "test/common/btif_storage_mock.cc",
+ "test/common/mock_csis_client.cc",
+ "test/common/mock_controller.cc",
+ "test/common/mock_device_groups.cc",
+ ],
+ shared_libs: [
+ "libprotobuf-cpp-lite",
+ "libcrypto",
+ "liblog",
+ ],
+ static_libs : [
+ "crypto_toolbox_for_tests",
+ "libgmock",
+ "libbt-common",
+ "libbt-protos-lite",
+ "libosi",
+ "liblc3codec",
+ ],
+ sanitize: {
+ cfi: true,
+ scs: true,
+ address: true,
+ all_undefined: true,
+ integer_overflow: true,
+ diag: {
+ undefined : true
+ },
+ },
+}
+
diff --git a/bta/csis/csis_client.cc b/bta/csis/csis_client.cc
index ec6318a0f..dffecf81a 100644
--- a/bta/csis/csis_client.cc
+++ b/bta/csis/csis_client.cc
@@ -1144,7 +1144,7 @@ class CsisClientImpl : public CsisClient {
if ((csis_group->GetDiscoveryState() !=
CsisDiscoveryState::CSIS_DISCOVERY_IDLE)) {
LOG(ERROR) << __func__
- << " Incorrect ase group: " << loghex(csis_group->GetGroupId())
+ << " Incorrect ase group: " << csis_group->GetGroupId()
<< " state "
<< loghex(static_cast<int>(csis_group->GetDiscoveryState()));
return;
diff --git a/bta/include/bta_le_audio_api.h b/bta/include/bta_le_audio_api.h
new file mode 100644
index 000000000..e4edcc137
--- /dev/null
+++ b/bta/include/bta_le_audio_api.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2019 HIMSA II K/S - www.himsa.com. Represented by EHIMA -
+ * www.ehima.com
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <base/callback_forward.h>
+#include <hardware/bt_le_audio.h>
+
+/* Interface class */
+class LeAudioClient {
+ public:
+ virtual ~LeAudioClient(void) = default;
+
+ static void Initialize(bluetooth::le_audio::LeAudioClientCallbacks* callbacks,
+ base::Closure initCb,
+ base::Callback<bool()> hal_2_1_verifier);
+ static void Cleanup(void);
+ static LeAudioClient* Get(void);
+ static void DebugDump(int fd);
+
+ virtual void RemoveDevice(const RawAddress& address) = 0;
+ virtual void Connect(const RawAddress& address) = 0;
+ virtual void Disconnect(const RawAddress& address) = 0;
+ virtual void GroupAddNode(const int group_id, const RawAddress& addr) = 0;
+ virtual void GroupRemoveNode(const int group_id, const RawAddress& addr) = 0;
+ virtual void GroupStream(const int group_id, const uint16_t content_type) = 0;
+ virtual void GroupSuspend(const int group_id) = 0;
+ virtual void GroupStop(const int group_id) = 0;
+ virtual void GroupDestroy(const int group_id) = 0;
+ virtual void GroupSetActive(const int group_id) = 0;
+ virtual std::vector<RawAddress> GetGroupDevices(const int group_id) = 0;
+ static void AddFromStorage(const RawAddress& addr, bool autoconnect);
+ static bool IsLeAudioClientRunning();
+};
diff --git a/bta/le_audio/client.cc b/bta/le_audio/client.cc
new file mode 100644
index 000000000..082609825
--- /dev/null
+++ b/bta/le_audio/client.cc
@@ -0,0 +1,2839 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com. Represented by EHIMA -
+ * www.ehima.com
+ *
+ * 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.
+ */
+
+#include <base/bind.h>
+#include <base/strings/string_number_conversions.h>
+
+#include "advertise_data_parser.h"
+#include "bta/csis/csis_types.h"
+#include "bta_api.h"
+#include "bta_gatt_api.h"
+#include "bta_gatt_queue.h"
+#include "bta_groups.h"
+#include "bta_le_audio_api.h"
+#include "btif_storage.h"
+#include "btm_iso_api.h"
+#include "client_audio.h"
+#include "client_parser.h"
+#include "device/include/controller.h"
+#include "devices.h"
+#include "embdrv/lc3/Api/Lc3Decoder.hpp"
+#include "embdrv/lc3/Api/Lc3Encoder.hpp"
+#include "gatt/bta_gattc_int.h"
+#include "le_audio_types.h"
+#include "osi/include/osi.h"
+#include "stack/btm/btm_dev.h"
+#include "stack/btm/btm_sec.h"
+#include "stack/include/btu.h" // do_in_main_thread
+#include "state_machine.h"
+
+using base::Closure;
+using bluetooth::Uuid;
+using bluetooth::groups::DeviceGroups;
+using bluetooth::groups::DeviceGroupsCallbacks;
+using bluetooth::hci::IsoManager;
+using bluetooth::hci::iso_manager::cig_create_cmpl_evt;
+using bluetooth::hci::iso_manager::cig_remove_cmpl_evt;
+using bluetooth::hci::iso_manager::CigCallbacks;
+using bluetooth::le_audio::ConnectionState;
+using bluetooth::le_audio::GroupNodeStatus;
+using bluetooth::le_audio::GroupStatus;
+using bluetooth::le_audio::GroupStreamStatus;
+using le_audio::LeAudioDevice;
+using le_audio::LeAudioDeviceGroup;
+using le_audio::LeAudioDeviceGroups;
+using le_audio::LeAudioDevices;
+using le_audio::LeAudioGroupStateMachine;
+using le_audio::types::ase;
+using le_audio::types::AseState;
+using le_audio::types::AudioContexts;
+using le_audio::types::AudioLocations;
+using le_audio::types::AudioStreamDataPathState;
+using le_audio::types::hdl_pair;
+using le_audio::types::kDefaultScanDurationS;
+using le_audio::types::LeAudioContextType;
+
+using le_audio::client_parser::ascs::
+ kCtpResponseCodeInvalidConfigurationParameterValue;
+using le_audio::client_parser::ascs::kCtpResponseCodeSuccess;
+using le_audio::client_parser::ascs::kCtpResponseInvalidAseCisMapping;
+using le_audio::client_parser::ascs::kCtpResponseNoReason;
+
+namespace {
+void le_audio_gattc_callback(tBTA_GATTC_EVT event, tBTA_GATTC* p_data);
+
+class LeAudioClientImpl;
+LeAudioClientImpl* instance;
+LeAudioClientAudioSinkReceiver* audioSinkReceiver;
+LeAudioClientAudioSourceReceiver* audioSourceReceiver;
+CigCallbacks* stateMachineHciCallbacks;
+LeAudioGroupStateMachine::Callbacks* stateMachineCallbacks;
+DeviceGroupsCallbacks* device_group_callbacks;
+
+/*
+ * Coordinatet Set Identification Profile (CSIP) based on CSIP 1.0
+ * and Coordinatet Set Identification Service (CSIS) 1.0
+ *
+ * CSIP allows to organize audio servers into sets e.g. Stereo Set, 5.1 Set
+ * and speed up connecting it.
+ *
+ * Since leaudio has already grouping API it was decided to integrate here CSIS
+ * and allow it to group devices semi-automatically.
+ *
+ * Flow:
+ * If connected device contains CSIS services, and it is included into CAP
+ * service, implementation marks device as a set member and waits for the
+ * bta/csis to learn about groups and notify implementation about assigned
+ * group id.
+ *
+ */
+/* LeAudioClientImpl class represents main implementation class for le audio
+ * feature in stack. This class implements GATT, le audio and ISO related parts.
+ *
+ * This class is represented in single instance and manages a group of devices,
+ * and devices. All devices calls back static method from it and are dispatched
+ * to target receivers (e.g. ASEs, devices).
+ *
+ * This instance also implements a LeAudioClient which is a upper layer API.
+ * Also LeAudioClientCallbacks are callbacks for upper layer.
+ *
+ * This class may be bonded with Test socket which allows to drive an instance
+ * for test purposes.
+ */
+class LeAudioClientImpl : public LeAudioClient {
+ public:
+ virtual ~LeAudioClientImpl() = default;
+
+ LeAudioClientImpl(
+ bluetooth::le_audio::LeAudioClientCallbacks* callbacks_,
+ LeAudioGroupStateMachine::Callbacks* state_machine_callbacks_,
+ base::Closure initCb)
+ : gatt_if_(0),
+ callbacks_(callbacks_),
+ active_group_id_(bluetooth::groups::kGroupUnknown),
+ current_context_type_(LeAudioContextType::MEDIA),
+ audio_sink_ready_to_receive(false),
+ audio_source_ready_to_send(false),
+ current_source_codec_config({0, 0, 0, 0}),
+ current_sink_codec_config({0, 0, 0, 0}),
+ lc3_encoder(nullptr),
+ lc3_decoder(nullptr),
+ audio_source_instance_(nullptr),
+ audio_sink_instance_(nullptr) {
+ LeAudioGroupStateMachine::Initialize(state_machine_callbacks_);
+ groupStateMachine_ = LeAudioGroupStateMachine::Get();
+
+ BTA_GATTC_AppRegister(
+ le_audio_gattc_callback,
+ base::Bind(
+ [](base::Closure initCb, uint8_t client_id, uint8_t status) {
+ if (status != GATT_SUCCESS) {
+ LOG(ERROR) << "Can't start LeAudio profile - no gatt "
+ "clients left!";
+ return;
+ }
+ instance->gatt_if_ = client_id;
+ initCb.Run();
+ },
+ initCb),
+ true);
+
+ DeviceGroups::Get()->Initialize(device_group_callbacks);
+ }
+
+ void AseInitialStateReadRequest(LeAudioDevice* leAudioDevice) {
+ int ases_num = leAudioDevice->ases_.size();
+ void* notify_flag_ptr = NULL;
+
+ for (int i = 0; i < ases_num; i++) {
+ /* Last read ase characteristic should issue connected state callback
+ * to upper layer
+ */
+
+ if (leAudioDevice->notify_connected_after_read_ &&
+ (i == (ases_num - 1))) {
+ notify_flag_ptr =
+ INT_TO_PTR(leAudioDevice->notify_connected_after_read_);
+ }
+
+ BtaGattQueue::ReadCharacteristic(leAudioDevice->conn_id_,
+ leAudioDevice->ases_[i].hdls.val_hdl,
+ OnGattReadRspStatic, notify_flag_ptr);
+ }
+ }
+
+ void OnGroupAddedCb(const RawAddress& address, const bluetooth::Uuid& uuid,
+ int group_id) {
+ LOG(INFO) << __func__ << " address: " << address << " group uuid " << uuid
+ << " group_id: " << group_id;
+
+ /* We are interested in the groups which are in the context of CAP */
+ if (uuid != le_audio::uuid::kCapServiceUuid) return;
+
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address);
+ if (!leAudioDevice) return;
+ if (leAudioDevice->group_id_ != bluetooth::groups::kGroupUnknown) {
+ LOG(INFO) << __func__
+ << " group already set: " << leAudioDevice->group_id_;
+ return;
+ }
+
+ group_add_node(group_id, address);
+ }
+
+ void OnGroupMemberAddedCb(const RawAddress& address, int group_id) {
+ LOG(INFO) << __func__ << " address: " << address
+ << " group_id: " << group_id;
+
+ auto group = aseGroups_.FindById(group_id);
+ if (!group) {
+ LOG(ERROR) << __func__ << " Not interested in group id: " << group_id;
+ return;
+ }
+
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address);
+ if (!leAudioDevice) return;
+ if (leAudioDevice->group_id_ != bluetooth::groups::kGroupUnknown) {
+ LOG(INFO) << __func__
+ << " group already set: " << leAudioDevice->group_id_;
+ return;
+ }
+
+ group_add_node(group_id, address);
+ }
+
+ void OnGroupMemberRemovedCb(const RawAddress& address, int group_id) {
+ LOG(INFO) << __func__ << " address: " << address
+ << " group_id: " << group_id;
+
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address);
+ if (!leAudioDevice) return;
+ if (leAudioDevice->group_id_ == bluetooth::groups::kGroupUnknown) {
+ LOG(INFO) << __func__ << " device already not assigned to the group.";
+ return;
+ }
+
+ LeAudioDeviceGroup* group = aseGroups_.FindById(group_id);
+ if (group == NULL) {
+ LOG(INFO) << __func__
+ << " device not in the group: " << leAudioDevice->address_
+ << ", " << group_id;
+ return;
+ }
+
+ group_remove_node(group, address);
+ }
+
+ /* This callback happens if kLeAudioDeviceSetStateTimeoutMs timeout happens
+ * during transition from origin to target state
+ */
+ void OnLeAudioDeviceSetStateTimeout(int group_id) {
+ LeAudioDeviceGroup* group = aseGroups_.FindById(group_id);
+
+ if (!group) {
+ /* Group removed */
+ return;
+ }
+
+ /* Releasement didn't finished in time */
+ if (group->GetTargetState() == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {
+ LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
+ LOG_ASSERT(leAudioDevice)
+ << __func__ << " Shouldn't be called without an active device.";
+
+ do {
+ if (instance) instance->DisconnectDevice(leAudioDevice, true);
+ leAudioDevice = group->GetNextActiveDevice(leAudioDevice);
+ } while (leAudioDevice);
+
+ return;
+ }
+
+ LOG(ERROR) << __func__ << ", State not achieved on time, releasing ases";
+
+ groupStateMachine_->StopStream(group);
+ }
+
+ void UpdateContextAndLocations(LeAudioDeviceGroup* group,
+ LeAudioDevice* leAudioDevice) {
+ std::optional<AudioContexts> new_group_updated_contexts =
+ group->UpdateActiveContextsMap(leAudioDevice->GetAvailableContexts());
+
+ if (new_group_updated_contexts || group->ReloadAudioLocations()) {
+ callbacks_->OnAudioConf(group->audio_directions_, group->group_id_,
+ group->snk_audio_locations_.to_ulong(),
+ group->src_audio_locations_.to_ulong(),
+ new_group_updated_contexts->to_ulong());
+ }
+ }
+
+ void CancelStreamingRequest() {
+ if (audio_source_ready_to_send) {
+ LeAudioClientAudioSource::CancelStreamingRequest();
+ audio_source_ready_to_send = false;
+ }
+
+ if (audio_sink_ready_to_receive) {
+ LeAudioClientAudioSink::CancelStreamingRequest();
+ audio_sink_ready_to_receive = false;
+ }
+ }
+
+ void ControlPointNotificationHandler(
+ struct le_audio::client_parser::ascs::ctp_ntf& ntf) {
+ for (auto& entry : ntf.entries) {
+ switch (entry.response_code) {
+ case kCtpResponseCodeInvalidConfigurationParameterValue:
+ switch (entry.reason) {
+ case kCtpResponseInvalidAseCisMapping:
+ CancelStreamingRequest();
+ break;
+ case kCtpResponseNoReason:
+ default:
+ break;
+ }
+ break;
+ case kCtpResponseCodeSuccess:
+ FALLTHROUGH;
+ default:
+ break;
+ }
+ }
+ }
+
+ void group_add_node(const int group_id, const RawAddress& address,
+ bool update_group_module = false) {
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address);
+ LeAudioDeviceGroup* new_group;
+ LeAudioDeviceGroup* old_group = nullptr;
+ int old_group_id = bluetooth::groups::kGroupUnknown;
+
+ if (!leAudioDevice) {
+ /* TODO This part possible to remove as this is to handle adding device to
+ * the group which is unknown and not connected.
+ */
+ LOG(INFO) << __func__ << ", leAudioDevice unknown , address: " << address
+ << " group: " << loghex(group_id);
+
+ if (group_id == bluetooth::groups::kGroupUnknown) return;
+
+ LOG(INFO) << __func__ << "Set member adding ...";
+ leAudioDevices_.Add(address, true);
+ leAudioDevice = leAudioDevices_.FindByAddress(address);
+ } else {
+ if (leAudioDevice->group_id_ != bluetooth::groups::kGroupUnknown) {
+ old_group = aseGroups_.FindById(leAudioDevice->group_id_);
+ old_group_id = old_group->group_id_;
+ }
+ }
+
+ auto id = DeviceGroups::Get()->GetGroupId(address,
+ le_audio::uuid::kCapServiceUuid);
+ if (group_id == bluetooth::groups::kGroupUnknown) {
+ if (id == bluetooth::groups::kGroupUnknown) {
+ DeviceGroups::Get()->AddDevice(address,
+ le_audio::uuid::kCapServiceUuid);
+ /* We will get back here when group will be created */
+ return;
+ }
+
+ new_group = aseGroups_.Add(id);
+ if (!new_group) {
+ LOG(ERROR) << __func__
+ << ", can't create group - group is already there?";
+ return;
+ }
+ } else {
+ LOG_ASSERT(id == group_id)
+ << " group id missmatch? leaudio id: " << group_id
+ << " groups module " << id;
+ new_group = aseGroups_.FindById(group_id);
+ if (!new_group) {
+ new_group = aseGroups_.Add(group_id);
+ } else {
+ if (new_group->IsDeviceInTheGroup(leAudioDevice)) return;
+ }
+ }
+
+ /* If device was in the group and it was not removed by the application,
+ * lets do it now
+ */
+ if (old_group) group_remove_node(old_group, address, update_group_module);
+
+ new_group->AddNode(leAudioDevices_.GetByAddress(address));
+
+ callbacks_->OnGroupNodeStatus(address, new_group->group_id_,
+ GroupNodeStatus::ADDED);
+
+ /* If device is connected and added to the group, lets read ASE states */
+ if (leAudioDevice->conn_id_ != GATT_INVALID_CONN_ID)
+ AseInitialStateReadRequest(leAudioDevice);
+
+ /* Group may be destroyed once moved its last node to new group */
+ if (aseGroups_.FindById(old_group_id) != nullptr) {
+ /* Removing node from group may touch its context integrity */
+ std::optional<AudioContexts> old_group_updated_contexts =
+ old_group->UpdateActiveContextsMap(old_group->GetActiveContexts());
+
+ if (old_group_updated_contexts || old_group->ReloadAudioLocations()) {
+ callbacks_->OnAudioConf(old_group->audio_directions_, old_group_id,
+ old_group->snk_audio_locations_.to_ulong(),
+ old_group->src_audio_locations_.to_ulong(),
+ old_group_updated_contexts->to_ulong());
+ }
+ }
+
+ UpdateContextAndLocations(new_group, leAudioDevice);
+ }
+
+ void GroupAddNode(const int group_id, const RawAddress& address) override {
+ auto id = DeviceGroups::Get()->GetGroupId(address,
+ le_audio::uuid::kCapServiceUuid);
+ if (id == group_id) return;
+
+ if (id != bluetooth::groups::kGroupUnknown) {
+ DeviceGroups::Get()->RemoveDevice(address, id);
+ }
+
+ DeviceGroups::Get()->AddDevice(address, le_audio::uuid::kCapServiceUuid,
+ group_id);
+ }
+
+ void group_remove_node(LeAudioDeviceGroup* group, const RawAddress& address,
+ bool update_group_module = false) {
+ int group_id = group->group_id_;
+ group->RemoveNode(leAudioDevices_.GetByAddress(address));
+
+ if (update_group_module) {
+ int groups_group_id = DeviceGroups::Get()->GetGroupId(
+ address, le_audio::uuid::kCapServiceUuid);
+ if (groups_group_id == group_id) {
+ DeviceGroups::Get()->RemoveDevice(address, group_id);
+ }
+ }
+
+ callbacks_->OnGroupNodeStatus(address, group_id, GroupNodeStatus::REMOVED);
+
+ /* Remove group if this was the last leAudioDevice in this group */
+ if (group->IsEmpty()) {
+ aseGroups_.Remove(group_id);
+
+ return;
+ }
+
+ /* Removing node from group touch its context integrity */
+ std::optional<AudioContexts> updated_contexts =
+ group->UpdateActiveContextsMap(group->GetActiveContexts());
+
+ if (updated_contexts || group->ReloadAudioLocations())
+ callbacks_->OnAudioConf(group->audio_directions_, group->group_id_,
+ group->snk_audio_locations_.to_ulong(),
+ group->src_audio_locations_.to_ulong(),
+ updated_contexts->to_ulong());
+ }
+
+ void GroupRemoveNode(const int group_id, const RawAddress& address) override {
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address);
+ LeAudioDeviceGroup* group = aseGroups_.FindById(group_id);
+
+ LOG(INFO) << __func__ << " group_id: " << group_id
+ << " address: " << address;
+
+ if (!leAudioDevice) {
+ LOG(ERROR) << __func__
+ << ", Skipping unknown leAudioDevice, address: " << address;
+ return;
+ }
+
+ if (leAudioDevice->group_id_ != group_id) {
+ LOG(ERROR) << __func__ << "Device is not in group_id: " << group_id
+ << ", but in group_id: " << leAudioDevice->group_id_;
+ return;
+ }
+
+ if (group == NULL) {
+ LOG(ERROR) << __func__ << " device not in the group ?!";
+ return;
+ }
+
+ group_remove_node(group, address, true);
+ }
+
+ void GroupStream(const int group_id, const uint16_t context_type) override {
+ LeAudioDeviceGroup* group = aseGroups_.FindById(group_id);
+ auto final_context_type = context_type;
+
+ if (context_type >= static_cast<uint16_t>(LeAudioContextType::RFU)) {
+ LOG(ERROR) << __func__ << ", stream context type is not supported: "
+ << loghex(context_type);
+ CancelStreamingRequest();
+ return;
+ }
+
+ if (!group) {
+ LOG(ERROR) << __func__ << ", unknown group id: " << group_id;
+ CancelStreamingRequest();
+ return;
+ }
+
+ auto supported_context_type = group->GetActiveContexts();
+ if (!(context_type & supported_context_type.to_ulong())) {
+ LOG(ERROR) << " Unsupported context type by remote device: "
+ << loghex(context_type) << ". Switching to unspecified";
+ final_context_type =
+ static_cast<uint16_t>(LeAudioContextType::UNSPECIFIED);
+ }
+
+ if (!group->IsAnyDeviceConnected()) {
+ LOG(ERROR) << __func__ << ", group " << group_id << " is not connected ";
+ CancelStreamingRequest();
+ return;
+ }
+
+ /* Check if any group is in the transition state. If so, we don't allow to
+ * start new group to stream */
+ if (aseGroups_.IsAnyInTransition()) {
+ LOG(INFO) << __func__ << " some group is already in the transition state";
+ CancelStreamingRequest();
+ return;
+ }
+
+ if (groupStateMachine_->StartStream(
+ group, static_cast<LeAudioContextType>(final_context_type)))
+ stream_request_started_ = true;
+ else
+ ClientAudioIntefraceRelease();
+ }
+
+ void GroupSuspend(const int group_id) override {
+ LeAudioDeviceGroup* group = aseGroups_.FindById(group_id);
+
+ if (!group) {
+ LOG(ERROR) << __func__ << ", unknown group id: " << group_id;
+ return;
+ }
+
+ if (!group->IsAnyDeviceConnected()) {
+ LOG(ERROR) << __func__ << ", group is not connected";
+ return;
+ }
+
+ if (group->IsInTransition()) {
+ LOG(INFO) << __func__
+ << ", group is in transition from: " << group->GetState()
+ << ", to: " << group->GetTargetState();
+ return;
+ }
+
+ if (group->GetState() != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ LOG(ERROR) << __func__
+ << ", invalid current state of group: " << group->GetState();
+ return;
+ }
+
+ audio_source_ready_to_send = false;
+ audio_sink_ready_to_receive = false;
+
+ groupStateMachine_->SuspendStream(group);
+ }
+
+ void GroupStop(const int group_id) override {
+ LeAudioDeviceGroup* group = aseGroups_.FindById(group_id);
+
+ if (!group) {
+ LOG(ERROR) << __func__ << ", unknown group id: " << group_id;
+ return;
+ }
+
+ if (group->IsEmpty()) {
+ LOG(ERROR) << __func__ << ", group is empty";
+ return;
+ }
+
+ if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {
+ LOG(ERROR) << __func__
+ << ", group already stopped: " << group->GetState();
+ return;
+ }
+
+ groupStateMachine_->StopStream(group);
+ }
+
+ void GroupDestroy(const int group_id) override {
+ LeAudioDeviceGroup* group = aseGroups_.FindById(group_id);
+
+ if (!group) {
+ LOG(ERROR) << __func__ << ", unknown group id: " << group_id;
+ return;
+ }
+
+ // Disconnect and remove each device within the group
+ auto* dev = group->GetFirstDevice();
+ while (dev) {
+ auto* next_dev = group->GetNextDevice(dev);
+ RemoveDevice(dev->address_);
+ dev = next_dev;
+ }
+ }
+
+ void GroupSetActive(const int group_id) override {
+ DLOG(INFO) << __func__ << " group_id: " << group_id;
+
+ if (group_id == bluetooth::groups::kGroupUnknown) {
+ if (active_group_id_ == bluetooth::groups::kGroupUnknown) {
+ /* Nothing to do */
+ return;
+ }
+
+ StopAudio();
+ GroupStop(active_group_id_);
+ callbacks_->OnGroupStatus(active_group_id_, GroupStatus::INACTIVE);
+ active_group_id_ = group_id;
+
+ return;
+ }
+
+ LeAudioDeviceGroup* group = aseGroups_.FindById(group_id);
+ if (!group) {
+ LOG(ERROR) << __func__
+ << ", Invalid group: " << static_cast<int>(group_id);
+ return;
+ }
+
+ if (active_group_id_ != bluetooth::groups::kGroupUnknown) {
+ LOG(WARNING) << __func__ << ", Another group already active: "
+ << static_cast<int>(active_group_id_);
+ return;
+ }
+
+ if (!audio_source_instance_) {
+ audio_source_instance_ = LeAudioClientAudioSource::Acquire();
+ if (!audio_source_instance_) {
+ LOG(ERROR) << __func__ << ", could not acquire audio source interface";
+ return;
+ }
+ }
+
+ if (!audio_sink_instance_) {
+ audio_sink_instance_ = LeAudioClientAudioSink::Acquire();
+ if (!audio_sink_instance_) {
+ LOG(ERROR) << __func__ << ", could not acquire audio sink interface";
+ LeAudioClientAudioSource::Release(audio_source_instance_);
+ return;
+ }
+ }
+
+ /* Configure audio HAL sessions with most frequent context */
+ UpdateCurrentHalSessions(group_id, LeAudioContextType::MEDIA);
+ if (current_source_codec_config.IsInvalid() &&
+ current_sink_codec_config.IsInvalid()) {
+ LOG(WARNING) << __func__ << ", unsupported device configurations";
+ callbacks_->OnGroupStatus(active_group_id_, GroupStatus::INACTIVE);
+ return;
+ }
+
+ active_group_id_ = group_id;
+ callbacks_->OnGroupStatus(active_group_id_, GroupStatus::ACTIVE);
+ }
+
+ void RemoveDevice(const RawAddress& address) override {
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address);
+ if (!leAudioDevice) {
+ return;
+ }
+
+ /* Remove the group assignment if not yet removed. It might happen that the
+ * group module has already called the appropriate callback and we have
+ * already removed the group assignment.
+ */
+ if (leAudioDevice->group_id_ != bluetooth::groups::kGroupUnknown) {
+ auto group = aseGroups_.FindById(leAudioDevice->group_id_);
+ group_remove_node(group, address, true);
+ }
+
+ if (leAudioDevice->conn_id_ != GATT_INVALID_CONN_ID) {
+ Disconnect(address);
+ leAudioDevice->removing_device_ = true;
+ return;
+ }
+
+ leAudioDevices_.Remove(address);
+ }
+
+ void Connect(const RawAddress& address) override {
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address);
+ if (!leAudioDevice) {
+ leAudioDevices_.Add(address, true);
+ } else {
+ leAudioDevice->connecting_actively_ = true;
+ }
+
+ BTA_GATTC_Open(gatt_if_, address, true, false);
+ }
+
+ std::vector<RawAddress> GetGroupDevices(const int group_id) override {
+ LeAudioDeviceGroup* group = aseGroups_.FindById(group_id);
+ std::vector<RawAddress> all_group_device_addrs;
+
+ if (group != nullptr) {
+ LeAudioDevice* leAudioDevice = group->GetFirstDevice();
+ while (leAudioDevice) {
+ all_group_device_addrs.push_back(leAudioDevice->address_);
+ leAudioDevice = group->GetNextDevice(leAudioDevice);
+ };
+ }
+
+ return all_group_device_addrs;
+ }
+
+ /* Restore paired device from storage to recreate groups */
+ void AddFromStorage(const RawAddress& address, bool autoconnect) {
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address);
+
+ LOG(INFO) << __func__ << ", restoring: " << address;
+
+ if (!leAudioDevice) {
+ leAudioDevices_.Add(address, false);
+ leAudioDevice = leAudioDevices_.FindByAddress(address);
+ }
+
+ int group_id = DeviceGroups::Get()->GetGroupId(
+ address, le_audio::uuid::kCapServiceUuid);
+ if (group_id != bluetooth::groups::kGroupUnknown) {
+ group_add_node(group_id, address);
+ }
+
+ if (autoconnect) Connect(address);
+ }
+
+ void Disconnect(const RawAddress& address) override {
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address);
+
+ if (!leAudioDevice) {
+ LOG(ERROR) << __func__ << ", leAudioDevice not connected (" << address
+ << ")";
+ return;
+ }
+
+ /* cancel pending direct connect */
+ if (leAudioDevice->connecting_actively_)
+ BTA_GATTC_CancelOpen(gatt_if_, address, true);
+
+ /* Removes all registrations for connection */
+ BTA_GATTC_CancelOpen(0, address, false);
+
+ if (leAudioDevice->conn_id_ == GATT_INVALID_CONN_ID) {
+ LOG(ERROR) << __func__ << ", leAudioDevice not connected (" << address
+ << ")";
+ return;
+ }
+
+ DisconnectDevice(leAudioDevice);
+ }
+
+ void DisconnectDevice(LeAudioDevice* leAudioDevice,
+ bool acl_force_disconnect = false) {
+ if (leAudioDevice->conn_id_ == GATT_INVALID_CONN_ID) {
+ return;
+ }
+
+ if (acl_force_disconnect) {
+ uint16_t acl_handle =
+ BTM_GetHCIConnHandle(leAudioDevice->address_, BT_TRANSPORT_LE);
+ if (acl_handle != HCI_INVALID_HANDLE) {
+ acl_disconnect_from_handle(acl_handle, HCI_ERR_PEER_USER);
+ return;
+ }
+ }
+
+ BtaGattQueue::Clean(leAudioDevice->conn_id_);
+ BTA_GATTC_Close(leAudioDevice->conn_id_);
+ leAudioDevice->conn_id_ = GATT_INVALID_CONN_ID;
+ }
+
+ void DeregisterNotifications(LeAudioDevice* leAudioDevice) {
+ /* GATTC will ommit not registered previously handles */
+ for (auto pac_tuple : leAudioDevice->snk_pacs_) {
+ BTA_GATTC_DeregisterForNotifications(gatt_if_, leAudioDevice->address_,
+ std::get<0>(pac_tuple).val_hdl);
+ }
+ for (auto pac_tuple : leAudioDevice->src_pacs_) {
+ BTA_GATTC_DeregisterForNotifications(gatt_if_, leAudioDevice->address_,
+ std::get<0>(pac_tuple).val_hdl);
+ }
+
+ if (leAudioDevice->snk_audio_locations_hdls_.val_hdl != 0)
+ BTA_GATTC_DeregisterForNotifications(
+ gatt_if_, leAudioDevice->address_,
+ leAudioDevice->snk_audio_locations_hdls_.val_hdl);
+ if (leAudioDevice->src_audio_locations_hdls_.val_hdl != 0)
+ BTA_GATTC_DeregisterForNotifications(
+ gatt_if_, leAudioDevice->address_,
+ leAudioDevice->src_audio_locations_hdls_.val_hdl);
+ if (leAudioDevice->audio_avail_hdls_.val_hdl != 0)
+ BTA_GATTC_DeregisterForNotifications(
+ gatt_if_, leAudioDevice->address_,
+ leAudioDevice->audio_avail_hdls_.val_hdl);
+ if (leAudioDevice->audio_supp_cont_hdls_.val_hdl != 0)
+ BTA_GATTC_DeregisterForNotifications(
+ gatt_if_, leAudioDevice->address_,
+ leAudioDevice->audio_supp_cont_hdls_.val_hdl);
+ if (leAudioDevice->ctp_hdls_.val_hdl != 0)
+ BTA_GATTC_DeregisterForNotifications(gatt_if_, leAudioDevice->address_,
+ leAudioDevice->ctp_hdls_.val_hdl);
+
+ for (struct ase& ase : leAudioDevice->ases_)
+ BTA_GATTC_DeregisterForNotifications(gatt_if_, leAudioDevice->address_,
+ ase.hdls.val_hdl);
+ }
+
+ /* This is a generic read/notify/indicate handler for gatt. Here messages
+ * are dispatched to correct elements e.g. ASEs, PACs, audio locations etc.
+ */
+ void LeAudioCharValueHandle(uint16_t conn_id, uint16_t hdl, uint16_t len,
+ uint8_t* value) {
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByConnId(conn_id);
+ struct ase* ase;
+
+ if (!leAudioDevice) {
+ LOG(ERROR) << __func__ << ", no leAudioDevice assigned to connection id: "
+ << static_cast<int>(conn_id);
+ return;
+ }
+
+ ase = leAudioDevice->GetAseByValHandle(hdl);
+
+ if (ase) {
+ LeAudioDeviceGroup* group = aseGroups_.FindById(leAudioDevice->group_id_);
+ groupStateMachine_->ProcessGattNotifEvent(value, len, ase, leAudioDevice,
+ group);
+
+ return;
+ }
+
+ auto snk_pac_ent = std::find_if(
+ leAudioDevice->snk_pacs_.begin(), leAudioDevice->snk_pacs_.end(),
+ [&hdl](auto& pac_ent) { return std::get<0>(pac_ent).val_hdl == hdl; });
+ if (snk_pac_ent != leAudioDevice->snk_pacs_.end()) {
+ std::vector<struct le_audio::types::acs_ac_record> pac_recs;
+
+ /* Guard consistency of PAC records structure */
+ if (!le_audio::client_parser::pacs::ParsePac(pac_recs, len, value))
+ return;
+
+ LOG(INFO) << __func__ << ", Registering sink PACs";
+ leAudioDevice->RegisterPACs(&std::get<1>(*snk_pac_ent), &pac_recs);
+
+ /* Update supported context types including internal capabilities */
+ LeAudioDeviceGroup* group = aseGroups_.FindById(leAudioDevice->group_id_);
+
+ /* Active context map should be considered to be updated in response to
+ * PACs update.
+ * Read of available context during initial attribute discovery.
+ * Group would be assigned once service search is completed.
+ */
+ if (group)
+ group->UpdateActiveContextsMap(leAudioDevice->GetAvailableContexts());
+
+ return;
+ }
+
+ auto src_pac_ent = std::find_if(
+ leAudioDevice->src_pacs_.begin(), leAudioDevice->src_pacs_.end(),
+ [&hdl](auto& pac_ent) { return std::get<0>(pac_ent).val_hdl == hdl; });
+ if (src_pac_ent != leAudioDevice->src_pacs_.end()) {
+ std::vector<struct le_audio::types::acs_ac_record> pac_recs;
+
+ /* Guard consistency of PAC records structure */
+ if (!le_audio::client_parser::pacs::ParsePac(pac_recs, len, value))
+ return;
+
+ LOG(INFO) << __func__ << ", Registering source PACs";
+ leAudioDevice->RegisterPACs(&std::get<1>(*src_pac_ent), &pac_recs);
+
+ /* Update supported context types including internal capabilities */
+ LeAudioDeviceGroup* group = aseGroups_.FindById(leAudioDevice->group_id_);
+
+ /* Active context map should be considered to be updated in response to
+ * PACs update.
+ * Read of available context during initial attribute discovery.
+ * Group would be assigned once service search is completed.
+ */
+ if (group)
+ group->UpdateActiveContextsMap(leAudioDevice->GetAvailableContexts());
+
+ return;
+ }
+
+ if (hdl == leAudioDevice->snk_audio_locations_hdls_.val_hdl) {
+ AudioLocations snk_audio_locations;
+
+ le_audio::client_parser::pacs::ParseAudioLocations(snk_audio_locations,
+ len, value);
+
+ /* Value may not change */
+ if ((leAudioDevice->audio_directions_ &
+ le_audio::types::kLeAudioDirectionSink) &&
+ (leAudioDevice->snk_audio_locations_ ^ snk_audio_locations).none())
+ return;
+
+ /* Presence of PAC characteristic for source means support for source
+ * audio location. Value of 0x00000000 means mono/unspecified
+ */
+ leAudioDevice->audio_directions_ |=
+ le_audio::types::kLeAudioDirectionSink;
+ leAudioDevice->snk_audio_locations_ = snk_audio_locations;
+
+ LeAudioDeviceGroup* group = aseGroups_.FindById(leAudioDevice->group_id_);
+ /* Read of source audio locations during initial attribute discovery.
+ * Group would be assigned once service search is completed.
+ */
+ if (group && group->ReloadAudioLocations()) {
+ callbacks_->OnAudioConf(group->audio_directions_, group->group_id_,
+ group->snk_audio_locations_.to_ulong(),
+ group->src_audio_locations_.to_ulong(),
+ group->GetActiveContexts().to_ulong());
+ }
+ } else if (hdl == leAudioDevice->src_audio_locations_hdls_.val_hdl) {
+ AudioLocations src_audio_locations;
+
+ le_audio::client_parser::pacs::ParseAudioLocations(src_audio_locations,
+ len, value);
+
+ /* Value may not change */
+ if ((leAudioDevice->audio_directions_ &
+ le_audio::types::kLeAudioDirectionSource) &&
+ (leAudioDevice->src_audio_locations_ ^ src_audio_locations).none())
+ return;
+
+ /* Presence of PAC characteristic for source means support for source
+ * audio location. Value of 0x00000000 means mono/unspecified
+ */
+ leAudioDevice->audio_directions_ |=
+ le_audio::types::kLeAudioDirectionSource;
+ leAudioDevice->src_audio_locations_ = src_audio_locations;
+
+ LeAudioDeviceGroup* group = aseGroups_.FindById(leAudioDevice->group_id_);
+ /* Read of source audio locations during initial attribute discovery.
+ * Group would be assigned once service search is completed.
+ */
+ if (group && group->ReloadAudioLocations()) {
+ callbacks_->OnAudioConf(group->audio_directions_, group->group_id_,
+ group->snk_audio_locations_.to_ulong(),
+ group->src_audio_locations_.to_ulong(),
+ group->GetActiveContexts().to_ulong());
+ }
+ } else if (hdl == leAudioDevice->audio_avail_hdls_.val_hdl) {
+ auto avail_audio_contexts = std::make_unique<
+ struct le_audio::client_parser::pacs::acs_available_audio_contexts>();
+
+ le_audio::client_parser::pacs::ParseAvailableAudioContexts(
+ *avail_audio_contexts, len, value);
+
+ auto updated_avail_contexts = leAudioDevice->SetAvailableContexts(
+ avail_audio_contexts->snk_avail_cont,
+ avail_audio_contexts->src_avail_cont);
+
+ if (updated_avail_contexts.any()) {
+ /* Update scenario map considering changed active context types */
+ LeAudioDeviceGroup* group =
+ aseGroups_.FindById(leAudioDevice->group_id_);
+ /* Read of available context during initial attribute discovery.
+ * Group would be assigned once service search is completed.
+ */
+ if (group) {
+ std::optional<AudioContexts> updated_contexts =
+ group->UpdateActiveContextsMap(updated_avail_contexts);
+ if (updated_contexts) {
+ callbacks_->OnAudioConf(group->audio_directions_, group->group_id_,
+ group->snk_audio_locations_.to_ulong(),
+ group->src_audio_locations_.to_ulong(),
+ updated_contexts->to_ulong());
+ }
+ }
+ }
+ } else if (hdl == leAudioDevice->audio_supp_cont_hdls_.val_hdl) {
+ auto supp_audio_contexts = std::make_unique<
+ struct le_audio::client_parser::pacs::acs_supported_audio_contexts>();
+
+ le_audio::client_parser::pacs::ParseSupportedAudioContexts(
+ *supp_audio_contexts, len, value);
+ /* Just store if for now */
+ leAudioDevice->SetSupportedContexts(supp_audio_contexts->snk_supp_cont,
+ supp_audio_contexts->src_supp_cont);
+ } else if (hdl == leAudioDevice->ctp_hdls_.val_hdl) {
+ auto ntf =
+ std::make_unique<struct le_audio::client_parser::ascs::ctp_ntf>();
+
+ if (ParseAseCtpNotification(*ntf, len, value))
+ ControlPointNotificationHandler(*ntf);
+ } else {
+ LOG(ERROR) << __func__ << ", Unknown attribute read: " << loghex(hdl);
+ }
+ }
+
+ void OnGattReadRsp(uint16_t conn_id, tGATT_STATUS status, uint16_t hdl,
+ uint16_t len, uint8_t* value, void* data) {
+ LeAudioCharValueHandle(conn_id, hdl, len, value);
+ }
+
+ void OnGattConnected(tGATT_STATUS status, uint16_t conn_id,
+ tGATT_IF client_if, RawAddress address,
+ tBT_TRANSPORT transport, uint16_t mtu) {
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address);
+
+ if (!leAudioDevice) return;
+
+ if (status != GATT_SUCCESS) {
+ /* autoconnect connection failed, that's ok */
+ if (!leAudioDevice->connecting_actively_) return;
+
+ LOG(ERROR) << "Failed to connect to LeAudio leAudioDevice, status: "
+ << +status;
+ callbacks_->OnConnectionState(ConnectionState::DISCONNECTED, address);
+ return;
+ }
+
+ if (controller_get_interface()->supports_ble_2m_phy()) {
+ LOG(INFO) << address << " set preferred PHY to 2M";
+ BTM_BleSetPhy(address, PHY_LE_2M, PHY_LE_2M, 0);
+ }
+
+ BTM_RequestPeerSCA(leAudioDevice->address_, transport);
+
+ leAudioDevice->connecting_actively_ = false;
+ leAudioDevice->conn_id_ = conn_id;
+
+ if (mtu == GATT_DEF_BLE_MTU_SIZE) {
+ LOG(INFO) << __func__ << ", Configure MTU";
+ BtaGattQueue::ConfigureMtu(leAudioDevice->conn_id_, 240);
+ }
+
+ /* If we know services, register for notifications */
+ if (leAudioDevice->known_service_handles_)
+ RegisterKnownNotifications(leAudioDevice);
+
+ if (BTM_SecIsSecurityPending(address)) {
+ /* if security collision happened, wait for encryption done
+ * (BTA_GATTC_ENC_CMPL_CB_EVT) */
+ return;
+ }
+
+ /* verify bond */
+ if (BTM_IsEncrypted(address, BT_TRANSPORT_LE)) {
+ /* if link has been encrypted */
+ OnEncryptionComplete(address, BTM_SUCCESS);
+ return;
+ }
+
+ if (BTM_IsLinkKeyKnown(address, BT_TRANSPORT_LE)) {
+ int result = BTM_SetEncryption(
+ address, BT_TRANSPORT_LE,
+ [](const RawAddress* bd_addr, tBT_TRANSPORT transport,
+ void* p_ref_data, tBTM_STATUS status) {
+ if (instance) instance->OnEncryptionComplete(*bd_addr, status);
+ },
+ nullptr, BTM_BLE_SEC_ENCRYPT);
+
+ LOG(INFO) << __func__
+ << "Encryption required. Request result: " << result;
+ return;
+ }
+
+ LOG(ERROR) << __func__ << " Encryption error";
+ }
+
+ void RegisterKnownNotifications(LeAudioDevice* leAudioDevice) {
+ LOG(INFO) << __func__ << " device: " << leAudioDevice->address_;
+
+ /* GATTC will ommit not registered previously handles */
+ for (auto pac_tuple : leAudioDevice->snk_pacs_) {
+ BTA_GATTC_RegisterForNotifications(gatt_if_, leAudioDevice->address_,
+ std::get<0>(pac_tuple).val_hdl);
+ }
+ for (auto pac_tuple : leAudioDevice->src_pacs_) {
+ BTA_GATTC_RegisterForNotifications(gatt_if_, leAudioDevice->address_,
+ std::get<0>(pac_tuple).val_hdl);
+ }
+
+ if (leAudioDevice->snk_audio_locations_hdls_.val_hdl != 0)
+ BTA_GATTC_RegisterForNotifications(
+ gatt_if_, leAudioDevice->address_,
+ leAudioDevice->snk_audio_locations_hdls_.val_hdl);
+ if (leAudioDevice->src_audio_locations_hdls_.val_hdl != 0)
+ BTA_GATTC_RegisterForNotifications(
+ gatt_if_, leAudioDevice->address_,
+ leAudioDevice->src_audio_locations_hdls_.val_hdl);
+ if (leAudioDevice->audio_avail_hdls_.val_hdl != 0)
+ BTA_GATTC_RegisterForNotifications(
+ gatt_if_, leAudioDevice->address_,
+ leAudioDevice->audio_avail_hdls_.val_hdl);
+ if (leAudioDevice->audio_supp_cont_hdls_.val_hdl != 0)
+ BTA_GATTC_RegisterForNotifications(
+ gatt_if_, leAudioDevice->address_,
+ leAudioDevice->audio_supp_cont_hdls_.val_hdl);
+ if (leAudioDevice->ctp_hdls_.val_hdl != 0)
+ BTA_GATTC_RegisterForNotifications(gatt_if_, leAudioDevice->address_,
+ leAudioDevice->ctp_hdls_.val_hdl);
+
+ for (struct ase& ase : leAudioDevice->ases_)
+ BTA_GATTC_RegisterForNotifications(gatt_if_, leAudioDevice->address_,
+ ase.hdls.val_hdl);
+ }
+
+ void OnEncryptionComplete(const RawAddress& address, uint8_t status) {
+ LOG(INFO) << __func__ << " " << address << "status: " << int{status};
+
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address);
+ if (leAudioDevice == NULL) {
+ LOG(WARNING) << "Skipping unknown device" << address;
+ return;
+ }
+
+ if (status != BTM_SUCCESS) {
+ LOG(ERROR) << "Encryption failed"
+ << " status: " << int{status};
+ BTA_GATTC_Close(leAudioDevice->conn_id_);
+ if (leAudioDevice->connecting_actively_) {
+ callbacks_->OnConnectionState(ConnectionState::DISCONNECTED, address);
+ }
+ return;
+ }
+
+ if (leAudioDevice->encrypted_) {
+ LOG(INFO) << __func__ << " link already encrypted, nothing to do";
+ return;
+ }
+
+ leAudioDevice->encrypted_ = true;
+
+ /* If we know services and read is not ongoing, this is reconnection and
+ * just notify connected */
+ if (leAudioDevice->known_service_handles_ &&
+ !leAudioDevice->notify_connected_after_read_) {
+ connectionReady(leAudioDevice);
+ return;
+ }
+
+ BTA_GATTC_ServiceSearchRequest(
+ leAudioDevice->conn_id_,
+ &le_audio::uuid::kPublishedAudioCapabilityServiceUuid);
+ }
+
+ void OnGattDisconnected(uint16_t conn_id, tGATT_IF client_if,
+ RawAddress address, tGATT_DISCONN_REASON reason) {
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address);
+
+ if (!leAudioDevice) {
+ LOG(ERROR) << ", skipping unknown leAudioDevice, address: " << address;
+ return;
+ }
+
+ LeAudioDeviceGroup* group = aseGroups_.FindById(leAudioDevice->group_id_);
+
+ groupStateMachine_->ProcessHciNotifAclDisconnected(group, leAudioDevice);
+
+ DeregisterNotifications(leAudioDevice);
+
+ callbacks_->OnConnectionState(ConnectionState::DISCONNECTED, address);
+ leAudioDevice->conn_id_ = GATT_INVALID_CONN_ID;
+ leAudioDevice->encrypted_ = false;
+
+ if (leAudioDevice->removing_device_) leAudioDevices_.Remove(address);
+ }
+
+ bool subscribe_for_indications(uint16_t conn_id, const RawAddress& address,
+ uint16_t handle, uint16_t ccc_handle,
+ bool ntf) {
+ std::vector<uint8_t> value(2);
+ uint8_t* ptr = value.data();
+
+ if (BTA_GATTC_RegisterForNotifications(gatt_if_, address, handle) !=
+ GATT_SUCCESS) {
+ LOG(ERROR) << __func__ << ", cannot register for notification: "
+ << static_cast<int>(handle);
+ return false;
+ }
+
+ UINT16_TO_STREAM(ptr, ntf ? GATT_CHAR_CLIENT_CONFIG_NOTIFICATION
+ : GATT_CHAR_CLIENT_CONFIG_INDICTION);
+
+ BtaGattQueue::WriteDescriptor(
+ conn_id, ccc_handle, std::move(value), GATT_WRITE,
+ [](uint16_t conn_id, tGATT_STATUS status, uint16_t handle, void* data) {
+ if (instance) instance->OnGattWriteCcc(conn_id, status, handle, data);
+ },
+ nullptr);
+ return true;
+ }
+
+ /* Find the handle for the client characteristics configuration of a given
+ * characteristics.
+ */
+ uint16_t find_ccc_handle(const gatt::Characteristic& charac) {
+ auto iter = std::find_if(
+ charac.descriptors.begin(), charac.descriptors.end(),
+ [](const auto& desc) {
+ return desc.uuid == Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG);
+ });
+
+ return iter == charac.descriptors.end() ? 0 : (*iter).handle;
+ }
+
+ void OnServiceChangeEvent(const RawAddress& address) {
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address);
+ if (!leAudioDevice) {
+ DLOG(ERROR) << __func__
+ << ", skipping unknown leAudioDevice, address: " << address;
+ return;
+ }
+
+ LOG(INFO) << __func__ << ": address=" << address;
+ leAudioDevice->known_service_handles_ = false;
+ leAudioDevice->csis_member_ = false;
+ BtaGattQueue::Clean(leAudioDevice->conn_id_);
+ DeregisterNotifications(leAudioDevice);
+ }
+
+ void OnGattServiceDiscoveryDone(const RawAddress& address) {
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address);
+ if (!leAudioDevice) {
+ DLOG(ERROR) << __func__
+ << ", skipping unknown leAudioDevice, address: " << address;
+ return;
+ }
+
+ if (!leAudioDevice->known_service_handles_)
+ BTA_GATTC_ServiceSearchRequest(
+ leAudioDevice->conn_id_,
+ &le_audio::uuid::kPublishedAudioCapabilityServiceUuid);
+ }
+ /* This method is called after connection beginning to identify and initialize
+ * a le audio device. Any missing mandatory attribute will result in reverting
+ * and cleaning up device.
+ */
+ void OnServiceSearchComplete(uint16_t conn_id, tGATT_STATUS status) {
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByConnId(conn_id);
+
+ if (!leAudioDevice) {
+ DLOG(ERROR) << __func__ << ", skipping unknown leAudioDevice, conn_id: "
+ << loghex(conn_id);
+ return;
+ }
+
+ LOG(INFO) << __func__ << " test csis_member "
+ << leAudioDevice->csis_member_;
+
+ if (status != GATT_SUCCESS) {
+ /* close connection and report service discovery complete with error */
+ LOG(ERROR) << "Service discovery failed";
+
+ DisconnectDevice(leAudioDevice);
+ return;
+ }
+
+ const std::list<gatt::Service>* services = BTA_GATTC_GetServices(conn_id);
+
+ const gatt::Service* pac_svc = nullptr;
+ const gatt::Service* ase_svc = nullptr;
+
+ for (const gatt::Service& tmp : *services) {
+ if (tmp.uuid == le_audio::uuid::kPublishedAudioCapabilityServiceUuid) {
+ LOG(INFO) << "Found Audio Capability service, handle: "
+ << loghex(tmp.handle);
+ pac_svc = &tmp;
+ } else if (tmp.uuid == le_audio::uuid::kAudioStreamControlServiceUuid) {
+ LOG(INFO) << "Found Audio Stream Endpoint service, handle: "
+ << loghex(tmp.handle);
+ ase_svc = &tmp;
+ } else if (tmp.uuid == le_audio::uuid::kCapServiceUuid) {
+ LOG(INFO) << "Found CAP Service, handle: " << loghex(tmp.handle);
+
+ /* Try to find context for CSIS instances */
+ for (auto& included_srvc : tmp.included_services) {
+ if (included_srvc.uuid == bluetooth::csis::kCsisServiceUuid) {
+ LOG(INFO) << __func__ << " CSIS included into CAS";
+ if (bluetooth::csis::CsisClient::IsCsisClientRunning())
+ leAudioDevice->csis_member_ = true;
+
+ break;
+ }
+ }
+ }
+ }
+
+ if (!pac_svc || !ase_svc) {
+ LOG(ERROR) << "No mandatory le audio services found";
+
+ DisconnectDevice(leAudioDevice);
+ return;
+ }
+
+ /* Refresh PACs handles */
+ leAudioDevice->ClearPACs();
+
+ for (const gatt::Characteristic& charac : pac_svc->characteristics) {
+ if (charac.uuid ==
+ le_audio::uuid::kSinkPublishedAudioCapabilityCharacteristicUuid) {
+ struct hdl_pair hdl_pair;
+ hdl_pair.val_hdl = charac.value_handle;
+ hdl_pair.ccc_hdl = find_ccc_handle(charac);
+
+ if (hdl_pair.ccc_hdl == 0) {
+ LOG(ERROR) << __func__ << ", snk pac char doesn't have ccc";
+
+ DisconnectDevice(leAudioDevice);
+ return;
+ }
+
+ if (!subscribe_for_indications(conn_id, leAudioDevice->address_,
+ hdl_pair.val_hdl, hdl_pair.ccc_hdl,
+ true)) {
+ DisconnectDevice(leAudioDevice);
+ return;
+ }
+
+ /* Obtain initial state of sink PACs */
+ BtaGattQueue::ReadCharacteristic(conn_id, hdl_pair.val_hdl,
+ OnGattReadRspStatic, NULL);
+
+ leAudioDevice->snk_pacs_.push_back(std::make_tuple(
+ hdl_pair, std::vector<struct le_audio::types::acs_ac_record>()));
+
+ LOG(INFO) << "Found Sink PAC characteristic, handle: "
+ << loghex(charac.value_handle)
+ << ", ccc handle: " << loghex(hdl_pair.ccc_hdl);
+ } else if (charac.uuid ==
+ le_audio::uuid::
+ kSourcePublishedAudioCapabilityCharacteristicUuid) {
+ struct hdl_pair hdl_pair;
+ hdl_pair.val_hdl = charac.value_handle;
+ hdl_pair.ccc_hdl = find_ccc_handle(charac);
+
+ if (hdl_pair.ccc_hdl == 0) {
+ LOG(ERROR) << __func__ << ", src pac char doesn't have ccc";
+
+ DisconnectDevice(leAudioDevice);
+ return;
+ }
+
+ if (!subscribe_for_indications(conn_id, leAudioDevice->address_,
+ hdl_pair.val_hdl, hdl_pair.ccc_hdl,
+ true)) {
+ DisconnectDevice(leAudioDevice);
+ return;
+ }
+
+ /* Obtain initial state of source PACs */
+ BtaGattQueue::ReadCharacteristic(conn_id, hdl_pair.val_hdl,
+ OnGattReadRspStatic, NULL);
+
+ leAudioDevice->src_pacs_.push_back(std::make_tuple(
+ hdl_pair, std::vector<struct le_audio::types::acs_ac_record>()));
+
+ LOG(INFO) << "Found Source PAC characteristic, handle: "
+ << loghex(charac.value_handle)
+ << ", ccc handle: " << loghex(hdl_pair.ccc_hdl);
+ } else if (charac.uuid ==
+ le_audio::uuid::kSinkAudioLocationCharacteristicUuid) {
+ leAudioDevice->snk_audio_locations_hdls_.val_hdl = charac.value_handle;
+ leAudioDevice->snk_audio_locations_hdls_.ccc_hdl =
+ find_ccc_handle(charac);
+
+ if (leAudioDevice->snk_audio_locations_hdls_.ccc_hdl == 0)
+ LOG(INFO) << __func__
+ << ", snk audio locations char doesn't have"
+ "ccc";
+
+ if (leAudioDevice->snk_audio_locations_hdls_.ccc_hdl != 0 &&
+ !subscribe_for_indications(
+ conn_id, leAudioDevice->address_,
+ leAudioDevice->snk_audio_locations_hdls_.val_hdl,
+ leAudioDevice->snk_audio_locations_hdls_.ccc_hdl, true)) {
+ DisconnectDevice(leAudioDevice);
+ return;
+ }
+
+ /* Obtain initial state of sink audio locations */
+ BtaGattQueue::ReadCharacteristic(
+ conn_id, leAudioDevice->snk_audio_locations_hdls_.val_hdl,
+ OnGattReadRspStatic, NULL);
+
+ LOG(INFO) << "Found Sink audio locations characteristic, handle: "
+ << loghex(charac.value_handle) << ", ccc handle: "
+ << loghex(leAudioDevice->snk_audio_locations_hdls_.ccc_hdl);
+ } else if (charac.uuid ==
+ le_audio::uuid::kSourceAudioLocationCharacteristicUuid) {
+ leAudioDevice->src_audio_locations_hdls_.val_hdl = charac.value_handle;
+ leAudioDevice->src_audio_locations_hdls_.ccc_hdl =
+ find_ccc_handle(charac);
+
+ if (leAudioDevice->src_audio_locations_hdls_.ccc_hdl == 0)
+ LOG(INFO) << __func__
+ << ", snk audio locations char doesn't have"
+ "ccc";
+
+ if (leAudioDevice->src_audio_locations_hdls_.ccc_hdl != 0 &&
+ !subscribe_for_indications(
+ conn_id, leAudioDevice->address_,
+ leAudioDevice->src_audio_locations_hdls_.val_hdl,
+ leAudioDevice->src_audio_locations_hdls_.ccc_hdl, true)) {
+ DisconnectDevice(leAudioDevice);
+ return;
+ }
+
+ /* Obtain initial state of source audio locations */
+ BtaGattQueue::ReadCharacteristic(
+ conn_id, leAudioDevice->src_audio_locations_hdls_.val_hdl,
+ OnGattReadRspStatic, NULL);
+
+ LOG(INFO) << "Found Source audio locations characteristic, handle: "
+ << loghex(charac.value_handle) << ", ccc handle: "
+ << loghex(leAudioDevice->src_audio_locations_hdls_.ccc_hdl);
+ } else if (charac.uuid ==
+ le_audio::uuid::kAudioContextAvailabilityCharacteristicUuid) {
+ leAudioDevice->audio_avail_hdls_.val_hdl = charac.value_handle;
+ leAudioDevice->audio_avail_hdls_.ccc_hdl = find_ccc_handle(charac);
+
+ if (leAudioDevice->audio_avail_hdls_.ccc_hdl == 0) {
+ LOG(ERROR) << __func__ << ", audio avails char doesn't have ccc";
+
+ DisconnectDevice(leAudioDevice);
+ return;
+ }
+
+ if (!subscribe_for_indications(conn_id, leAudioDevice->address_,
+ leAudioDevice->audio_avail_hdls_.val_hdl,
+ leAudioDevice->audio_avail_hdls_.ccc_hdl,
+ true)) {
+ DisconnectDevice(leAudioDevice);
+ return;
+ }
+
+ /* Obtain initial state */
+ BtaGattQueue::ReadCharacteristic(
+ conn_id, leAudioDevice->audio_avail_hdls_.val_hdl,
+ OnGattReadRspStatic, NULL);
+
+ LOG(INFO) << "Found Audio Availability Context characteristic, handle: "
+ << loghex(charac.value_handle) << ", ccc handle: "
+ << loghex(leAudioDevice->audio_avail_hdls_.ccc_hdl);
+ } else if (charac.uuid ==
+ le_audio::uuid::kAudioSupportedContextCharacteristicUuid) {
+ leAudioDevice->audio_supp_cont_hdls_.val_hdl = charac.value_handle;
+ leAudioDevice->audio_supp_cont_hdls_.ccc_hdl = find_ccc_handle(charac);
+
+ if (leAudioDevice->audio_supp_cont_hdls_.ccc_hdl == 0)
+ LOG(INFO) << __func__ << ", audio avails char doesn't have ccc";
+
+ if (leAudioDevice->audio_supp_cont_hdls_.ccc_hdl != 0 &&
+ !subscribe_for_indications(
+ conn_id, leAudioDevice->address_,
+ leAudioDevice->audio_supp_cont_hdls_.val_hdl,
+ leAudioDevice->audio_supp_cont_hdls_.ccc_hdl, true)) {
+ DisconnectDevice(leAudioDevice);
+ return;
+ }
+
+ /* Obtain initial state */
+ BtaGattQueue::ReadCharacteristic(
+ conn_id, leAudioDevice->audio_supp_cont_hdls_.val_hdl,
+ OnGattReadRspStatic, NULL);
+
+ LOG(INFO) << "Found Audio Supported Context characteristic, handle: "
+ << loghex(charac.value_handle) << ", ccc handle: "
+ << loghex(leAudioDevice->audio_supp_cont_hdls_.ccc_hdl);
+ }
+ }
+
+ /* Refresh ASE handles */
+ leAudioDevice->ases_.clear();
+
+ for (const gatt::Characteristic& charac : ase_svc->characteristics) {
+ LOG(INFO) << "Found characteristic, uuid: " << charac.uuid.ToString();
+ if (charac.uuid == le_audio::uuid::kSinkAudioStreamEndpointUuid ||
+ charac.uuid == le_audio::uuid::kSourceAudioStreamEndpointUuid) {
+ uint16_t ccc_handle = find_ccc_handle(charac);
+ if (ccc_handle == 0) {
+ LOG(ERROR) << __func__ << ", audio avails char doesn't have ccc";
+
+ DisconnectDevice(leAudioDevice);
+ return;
+ }
+
+ if (!subscribe_for_indications(conn_id, leAudioDevice->address_,
+ charac.value_handle, ccc_handle, true)) {
+ DisconnectDevice(leAudioDevice);
+ return;
+ }
+
+ int direction =
+ charac.uuid == le_audio::uuid::kSinkAudioStreamEndpointUuid
+ ? le_audio::types::kLeAudioDirectionSink
+ : le_audio::types::kLeAudioDirectionSource;
+
+ leAudioDevice->ases_.emplace_back(charac.value_handle, ccc_handle,
+ direction);
+
+ LOG(INFO) << "Found ASE characteristic, handle: "
+ << loghex(charac.value_handle)
+ << ", ccc handle: " << loghex(ccc_handle)
+ << ", direction: " << direction;
+ } else if (charac.uuid ==
+ le_audio::uuid::
+ kAudioStreamEndpointControlPointCharacteristicUuid) {
+ leAudioDevice->ctp_hdls_.val_hdl = charac.value_handle;
+ leAudioDevice->ctp_hdls_.ccc_hdl = find_ccc_handle(charac);
+
+ if (leAudioDevice->ctp_hdls_.ccc_hdl == 0) {
+ LOG(ERROR) << __func__ << ", ase ctp doesn't have ccc";
+
+ DisconnectDevice(leAudioDevice);
+ return;
+ }
+
+ if (!subscribe_for_indications(conn_id, leAudioDevice->address_,
+ leAudioDevice->ctp_hdls_.val_hdl,
+ leAudioDevice->ctp_hdls_.ccc_hdl,
+ true)) {
+ DisconnectDevice(leAudioDevice);
+ return;
+ }
+
+ LOG(INFO) << "Found ASE Control Point characteristic, handle: "
+ << loghex(charac.value_handle) << ", ccc handle: "
+ << loghex(leAudioDevice->ctp_hdls_.ccc_hdl);
+ }
+ }
+
+ leAudioDevice->known_service_handles_ = true;
+ leAudioDevice->notify_connected_after_read_ = true;
+
+ /* If already known group id */
+ if (leAudioDevice->group_id_ != bluetooth::groups::kGroupUnknown) {
+ AseInitialStateReadRequest(leAudioDevice);
+ return;
+ }
+
+ /* If device does not belong to any group yet we either add it to the
+ * group by our selfs now or wait for Csis to do it. In both cases, let's
+ * check if group is already assigned.
+ */
+ int group_id = DeviceGroups::Get()->GetGroupId(
+ leAudioDevice->address_, le_audio::uuid::kCapServiceUuid);
+ if (group_id != bluetooth::groups::kGroupUnknown) {
+ instance->group_add_node(group_id, leAudioDevice->address_);
+ return;
+ }
+
+ /* CSIS will trigger adding to group */
+ if (leAudioDevice->csis_member_) {
+ LOG(INFO) << __func__ << " waiting for CSIS to create group for device "
+ << leAudioDevice->address_;
+ return;
+ }
+
+ /* If there is no Csis just add device by our own */
+ DeviceGroups::Get()->AddDevice(leAudioDevice->address_,
+ le_audio::uuid::kCapServiceUuid);
+ }
+
+ void OnGattWriteCcc(uint16_t conn_id, tGATT_STATUS status, uint16_t hdl,
+ void* data) {
+ LeAudioDevice* leAudioDevice = leAudioDevices_.FindByConnId(conn_id);
+ std::vector<struct ase>::iterator ase_it;
+
+ if (!leAudioDevice) {
+ LOG(ERROR) << __func__ << ", unknown conn_id=" << loghex(conn_id);
+ return;
+ }
+
+ if (status == GATT_SUCCESS) {
+ LOG(INFO) << __func__
+ << ", successfully registered on ccc: " << loghex(hdl);
+ return;
+ }
+
+ LOG(ERROR) << __func__
+ << ", Failed to register for indications: " << loghex(hdl)
+ << ", status: " << loghex((int)(status));
+
+ ase_it =
+ std::find_if(leAudioDevice->ases_.begin(), leAudioDevice->ases_.end(),
+ [&hdl](const struct ase& ase) -> bool {
+ return ase.hdls.ccc_hdl == hdl;
+ });
+
+ if (ase_it == leAudioDevice->ases_.end()) {
+ LOG(ERROR) << __func__
+ << ", unknown ccc handle: " << static_cast<int>(hdl);
+ return;
+ }
+
+ BTA_GATTC_DeregisterForNotifications(gatt_if_, leAudioDevice->address_,
+ ase_it->hdls.val_hdl);
+ }
+
+ void AttachToStreamingGroupIfNeeded(LeAudioDevice* leAudioDevice) {
+ if (leAudioDevice->group_id_ != active_group_id_) {
+ LOG(INFO) << __func__ << " group " << leAudioDevice->group_id_
+ << " is not streaming. Nothing to do";
+ return;
+ }
+
+ LOG(INFO) << __func__ << " attaching to group "
+ << leAudioDevice->group_id_;
+
+ /* Restore configuration */
+ LeAudioDeviceGroup* group = aseGroups_.FindById(active_group_id_);
+ auto* stream_conf = &group->stream_conf;
+
+ if (!stream_conf->valid) {
+ LOG(ERROR) << __func__
+ << " Configuration not valid. (btw not sure we need this "
+ "flag)";
+ return;
+ }
+
+ le_audio::types::AudioLocations sink_group_audio_locations = 0;
+ uint8_t sink_num_of_active_ases = 0;
+
+ for (auto [cis_handle, audio_location] : stream_conf->sink_streams) {
+ sink_group_audio_locations |= audio_location;
+ sink_num_of_active_ases++;
+ }
+
+ le_audio::types::AudioLocations source_group_audio_locations = 0;
+ uint8_t source_num_of_active_ases = 0;
+
+ for (auto [cis_handle, audio_location] : stream_conf->source_streams) {
+ source_group_audio_locations |= audio_location;
+ source_num_of_active_ases++;
+ }
+
+ for (auto& ent : stream_conf->conf->confs) {
+ if (ent.direction == le_audio::types::kLeAudioDirectionSink) {
+ /* Sink*/
+ if (!leAudioDevice->ConfigureAses(ent, group->GetCurrentContextType(),
+ &sink_num_of_active_ases,
+ sink_group_audio_locations,
+ source_group_audio_locations, true)) {
+ LOG(INFO) << __func__ << " Could not set sink configuration of "
+ << stream_conf->conf->name;
+ return;
+ }
+ } else {
+ /* Source*/
+ if (!leAudioDevice->ConfigureAses(ent, group->GetCurrentContextType(),
+ &source_num_of_active_ases,
+ sink_group_audio_locations,
+ source_group_audio_locations, true)) {
+ LOG(INFO) << __func__ << " Could not set source configuration of "
+ << stream_conf->conf->name;
+ return;
+ }
+ }
+ }
+
+ groupStateMachine_->AttachToStream(group, leAudioDevice);
+ }
+
+ void connectionReady(LeAudioDevice* leAudioDevice) {
+ callbacks_->OnConnectionState(ConnectionState::CONNECTED,
+ leAudioDevice->address_);
+
+ if (leAudioDevice->group_id_ != bluetooth::groups::kGroupUnknown) {
+ LeAudioDeviceGroup* group = aseGroups_.FindById(leAudioDevice->group_id_);
+ UpdateContextAndLocations(group, leAudioDevice);
+ AttachToStreamingGroupIfNeeded(leAudioDevice);
+ }
+
+ if (leAudioDevice->first_connection_) {
+ btif_storage_set_leaudio_autoconnect(leAudioDevice->address_, true);
+ leAudioDevice->first_connection_ = false;
+ }
+ }
+
+ bool IsAseAcceptingAudioData(struct ase* ase) {
+ if (ase == nullptr) return false;
+ if (ase->state != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) return false;
+ if (ase->data_path_state != AudioStreamDataPathState::DATA_PATH_ESTABLISHED)
+ return false;
+
+ return true;
+ }
+
+ void get_left_and_right_stream(const std::vector<uint8_t>& data,
+ std::vector<int16_t>& chan_left,
+ std::vector<int16_t>& chan_right,
+ bool prepare_mono = false) {
+ uint16_t num_of_frames_per_ch = lc3_encoder->lc3Config.NF;
+
+ chan_left.reserve(num_of_frames_per_ch);
+ chan_right.reserve(num_of_frames_per_ch);
+ for (int i = 0; i < num_of_frames_per_ch; i++) {
+ const uint8_t* sample = data.data() + i * 4;
+
+ int16_t left = (int16_t)((*(sample + 1) << 8) + *sample) >> 1;
+
+ sample += 2;
+ int16_t right = (int16_t)((*(sample + 1) << 8) + *sample) >> 1;
+
+ if (prepare_mono) {
+ uint16_t mono_data = (int16_t)(((uint32_t)left + (uint32_t)right) >> 1);
+ left = mono_data;
+ right = mono_data;
+ }
+
+ chan_left.push_back(left);
+ chan_right.push_back(right);
+ }
+ }
+
+ void PrepareAndSendToTwoDevices(
+ const std::vector<uint8_t>& data,
+ struct le_audio::stream_configuration* stream_conf) {
+ uint16_t byte_count = stream_conf->sink_octets_per_codec_frame;
+ uint16_t left_cis_handle = 0;
+ uint16_t right_cis_handle = 0;
+ uint16_t number_of_required_samples_per_channel = lc3_encoder->lc3Config.NF;
+
+ for (auto [cis_handle, audio_location] : stream_conf->sink_streams) {
+ if (audio_location & le_audio::codec_spec_conf::kLeAudioLocationAnyLeft)
+ left_cis_handle = cis_handle;
+ if (audio_location & le_audio::codec_spec_conf::kLeAudioLocationAnyRight)
+ right_cis_handle = cis_handle;
+ }
+
+ if (data.size() < 2 /* bytes per sample */ * 2 /* channels */ *
+ number_of_required_samples_per_channel) {
+ LOG(ERROR) << __func__ << "Missing samples";
+ return;
+ }
+
+ bool mono = (left_cis_handle == 0) || (right_cis_handle == 0);
+ std::vector<int16_t> chan_left;
+ std::vector<int16_t> chan_right;
+ get_left_and_right_stream(data, chan_left, chan_right, mono);
+
+ std::vector<uint8_t> chan_left_enc(byte_count, 0);
+ std::vector<uint8_t> chan_right_enc(byte_count, 0);
+
+ uint8_t err = 0;
+ if (left_cis_handle)
+ err |= lc3_encoder->run((const int16_t*)chan_left.data(),
+ chan_left_enc.size(), chan_left_enc.data(), 0);
+ if (right_cis_handle)
+ err |= lc3_encoder->run((const int16_t*)chan_right.data(),
+ chan_right_enc.size(), chan_right_enc.data(), 1);
+
+ if (err != Lc3Encoder::ERROR_FREE) {
+ LOG(ERROR) << " error while encoding; error code: "
+ << "\t encoded samples: " << chan_left_enc.size()
+ << "\t err: " << static_cast<uint8_t>(err);
+ return;
+ }
+
+ /* Send data to the controller */
+ if (left_cis_handle)
+ IsoManager::GetInstance()->SendIsoData(
+ left_cis_handle, chan_left_enc.data(), chan_left_enc.size());
+
+ if (right_cis_handle)
+ IsoManager::GetInstance()->SendIsoData(
+ right_cis_handle, chan_right_enc.data(), chan_right_enc.size());
+ }
+
+ void PrepareAndSendToSingleDevice(
+ const std::vector<uint8_t>& data,
+ struct le_audio::stream_configuration* stream_conf) {
+ int num_channels = lc3_encoder->lc3Config.Nc;
+ uint16_t byte_count = stream_conf->sink_octets_per_codec_frame;
+ auto cis_handle = stream_conf->sink_streams.front().first;
+ uint16_t number_of_required_samples_per_channel = lc3_encoder->lc3Config.NF;
+
+ if ((int)data.size() < (2 /* bytes per sample */ * num_channels *
+ number_of_required_samples_per_channel)) {
+ LOG(ERROR) << __func__ << "Missing samples";
+ return;
+ }
+
+ std::vector<uint8_t> chan_encoded(num_channels * byte_count, 0);
+ uint8_t err = 0;
+ if (num_channels == 1) {
+ err = lc3_encoder->run((const int16_t*)data.data(), byte_count,
+ chan_encoded.data(), 0);
+
+ } else {
+ std::vector<int16_t> chan_left;
+ std::vector<int16_t> chan_right;
+ get_left_and_right_stream(data, chan_left, chan_right, false);
+
+ err |= lc3_encoder->run((const int16_t*)chan_left.data(), byte_count,
+ chan_encoded.data(), 0);
+
+ err |= lc3_encoder->run((const int16_t*)chan_right.data(), byte_count,
+ chan_encoded.data() + byte_count, 1);
+ }
+
+ if (err != Lc3Encoder::ERROR_FREE) {
+ LOG(ERROR) << " error while encoding; error code: "
+ << "\t err: " << static_cast<uint8_t>(err);
+ return;
+ }
+
+ /* Send data to the controller */
+ IsoManager::GetInstance()->SendIsoData(cis_handle, chan_encoded.data(),
+ chan_encoded.size());
+ }
+
+ struct le_audio::stream_configuration* GetStreamSinkConfiguration(
+ LeAudioDeviceGroup* group) {
+ struct le_audio::stream_configuration* stream_conf = &group->stream_conf;
+ int num_of_devices = 0;
+ int num_of_channels = 0;
+ uint32_t sample_freq_hz = 0;
+ uint32_t frame_duration_us = 0;
+ uint16_t octets_per_frame = 0;
+
+ LOG(INFO) << __func__ << " group_id: " << group->group_id_;
+
+ /* This contains pair of cis handle and audio location */
+ std::vector<std::pair<uint16_t, uint32_t>> streams;
+
+ for (auto* device = group->GetFirstActiveDevice(); device != nullptr;
+ device = group->GetNextActiveDevice(device)) {
+ auto* ase = device->GetFirstActiveAseByDirection(
+ le_audio::types::kLeAudioDirectionSink);
+
+ if (ase) {
+ LOG(INFO) << __func__ << "device: " << device->address_;
+ num_of_devices++;
+ }
+
+ for (; ase != nullptr;
+ ase = device->GetNextActiveAseWithSameDirection(ase)) {
+ streams.emplace_back(std::make_pair(
+ ase->cis_conn_hdl, ase->codec_config.audio_channel_allocation));
+ num_of_channels += ase->codec_config.channel_count;
+ if (sample_freq_hz == 0) {
+ sample_freq_hz = ase->codec_config.GetSamplingFrequencyHz();
+ } else {
+ LOG_ASSERT(sample_freq_hz ==
+ ase->codec_config.GetSamplingFrequencyHz())
+ << __func__ << " sample freq mismatch: " << +sample_freq_hz
+ << " != " << ase->codec_config.GetSamplingFrequencyHz();
+ }
+
+ if (frame_duration_us == 0) {
+ frame_duration_us = ase->codec_config.GetFrameDurationUs();
+ } else {
+ LOG_ASSERT(frame_duration_us ==
+ ase->codec_config.GetFrameDurationUs())
+ << __func__ << " frame duration mismatch: " << +frame_duration_us
+ << " != " << ase->codec_config.GetFrameDurationUs();
+ }
+
+ if (octets_per_frame == 0) {
+ octets_per_frame = ase->codec_config.octets_per_codec_frame;
+ } else {
+ LOG_ASSERT(octets_per_frame ==
+ ase->codec_config.octets_per_codec_frame)
+ << __func__ << " octets per frame mismatch: " << +octets_per_frame
+ << " != " << ase->codec_config.octets_per_codec_frame;
+ }
+
+ LOG(INFO) << __func__ << " Added CIS: " << +ase->cis_conn_hdl
+ << " to stream. Allocation: "
+ << +ase->codec_config.audio_channel_allocation
+ << " sample_freq: " << +sample_freq_hz
+ << " frame_duration: " << +frame_duration_us
+ << " octects per frame: " << +octets_per_frame;
+ }
+ }
+
+ if (streams.empty()) return nullptr;
+
+ stream_conf->sink_streams = std::move(streams);
+ stream_conf->sink_num_of_devices = num_of_devices;
+ stream_conf->sink_num_of_channels = num_of_channels;
+ stream_conf->sink_sample_frequency_hz = sample_freq_hz;
+ stream_conf->sink_frame_duration_us = frame_duration_us;
+ stream_conf->sink_octets_per_codec_frame = octets_per_frame;
+ stream_conf->valid = true;
+ stream_conf->conf = group->GetActiveConfiguration();
+
+ LOG(INFO) << __func__ << " configuration: " << stream_conf->conf->name;
+
+ return stream_conf;
+ }
+
+ void OnAudioDataReady(const std::vector<uint8_t>& data) {
+ if (active_group_id_ == bluetooth::groups::kGroupUnknown ||
+ !audio_source_ready_to_send)
+ return;
+
+ LeAudioDeviceGroup* group = aseGroups_.FindById(active_group_id_);
+ if (!group) {
+ LOG(ERROR) << __func__ << "There is no streaming group available";
+ return;
+ }
+
+ auto stream_conf = group->stream_conf;
+ if (!stream_conf.valid || (stream_conf.sink_num_of_devices > 2)) {
+ LOG(ERROR) << __func__ << " Stream configufation is not valid.";
+ return;
+ }
+
+ if (stream_conf.sink_num_of_devices == 2) {
+ PrepareAndSendToTwoDevices(data, &stream_conf);
+ } else {
+ PrepareAndSendToSingleDevice(data, &stream_conf);
+ }
+ }
+
+ void SendAudioData(uint8_t* data, uint16_t size) {
+ /* Get only one channel for MONO microphone */
+ /* Gather data for channel */
+ uint16_t required_for_channel_byte_count =
+ lc3_decoder->lc3Config.getByteCountFromBitrate(32000);
+ size_t required_byte_count = current_sink_codec_config.num_channels *
+ required_for_channel_byte_count;
+
+ if (required_byte_count != size) {
+ LOG(ERROR) << "Insufficient data for decoding and send, required: "
+ << int(required_byte_count) << ", received: " << int(size);
+ return;
+ }
+
+ uint8_t BEC_detect = 0;
+ std::vector<int16_t> pcm_data_decoded(lc3_decoder->lc3Config.NF, 0);
+ auto err = lc3_decoder->run(data, required_for_channel_byte_count, 0,
+ pcm_data_decoded.data(),
+ pcm_data_decoded.size(), BEC_detect);
+
+ /* TODO: How handle failing decoding ? */
+ if (err != Lc3Decoder::ERROR_FREE) {
+ LOG(ERROR) << " error while decoding error code: "
+ << static_cast<int>(err);
+ return;
+ }
+
+ uint16_t to_write = sizeof(int16_t) * pcm_data_decoded.size();
+ uint16_t written = LeAudioClientAudioSink::SendData(
+ (uint8_t*)pcm_data_decoded.data(), to_write);
+
+ /* TODO: What to do if not all data sinked ? */
+ if (written != to_write) LOG(ERROR) << __func__ << ", not all data sinked";
+
+ LOG(INFO) << __func__;
+ }
+
+ static inline Lc3Config::FrameDuration Lc3ConfigFrameDuration(
+ uint32_t frame_duration_us) {
+ if (frame_duration_us == LeAudioCodecConfiguration::kInterval7500Us)
+ return Lc3Config::FrameDuration::d7p5ms;
+ else
+ return Lc3Config::FrameDuration::d10ms;
+ }
+
+ bool StartSendingAudio(int group_id) {
+ LOG(INFO) << __func__;
+
+ LeAudioDeviceGroup* group = aseGroups_.FindById(group_id);
+ LeAudioDevice* device = group->GetFirstActiveDevice();
+ LOG_ASSERT(device) << __func__
+ << " Shouldn't be called without an active device.";
+
+ /* Assume 2 ases max just for now. */
+ auto* stream_conf = GetStreamSinkConfiguration(group);
+ if (stream_conf == nullptr) {
+ LOG(ERROR) << __func__ << " could not get sink configuration";
+ return false;
+ }
+
+ if (lc3_encoder) {
+ LOG(WARNING)
+ << " The encoder instance should have been already released.";
+ delete lc3_encoder;
+ lc3_encoder = nullptr;
+ }
+
+ /* One or multiple audio channels encoder */
+ lc3_encoder = new Lc3Encoder(Lc3Config(
+ current_source_codec_config.sample_rate,
+ Lc3ConfigFrameDuration(current_source_codec_config.data_interval_us),
+ current_source_codec_config.num_channels));
+
+ uint16_t remote_delay_ms =
+ group->GetRemoteDelay(le_audio::types::kLeAudioDirectionSink);
+
+ LeAudioClientAudioSource::UpdateRemoteDelay(remote_delay_ms);
+ LeAudioClientAudioSource::ConfirmStreamingRequest();
+
+ return true;
+ }
+
+ struct le_audio::stream_configuration* GetStreamSourceConfiguration(
+ LeAudioDeviceGroup* group) {
+ LeAudioDevice* device = group->GetFirstActiveDevice();
+ LOG_ASSERT(device) << __func__
+ << " Shouldn't be called without an active device.";
+
+ ase* ase = device->GetFirstActiveAseByDirection(
+ le_audio::types::kLeAudioDirectionSource);
+
+ if (!ase) return nullptr;
+
+ /* For now we support one microphone only*/
+
+ auto* stream_conf = &group->stream_conf;
+ std::vector<std::pair<uint16_t, uint32_t>> streams;
+
+ stream_conf->source_streams.emplace_back(std::make_pair(
+ ase->cis_conn_hdl, ase->codec_config.audio_channel_allocation));
+
+ stream_conf->source_num_of_devices = 1;
+ stream_conf->source_num_of_channels = 1;
+ stream_conf->source_sample_frequency_hz =
+ ase->codec_config.GetSamplingFrequencyHz();
+ stream_conf->source_frame_duration_us =
+ ase->codec_config.GetFrameDurationUs();
+ stream_conf->source_octets_per_codec_frame =
+ ase->codec_config.octets_per_codec_frame;
+ stream_conf->valid = true;
+
+ stream_conf->conf = group->GetActiveConfiguration();
+
+ LOG(INFO) << __func__ << " Added CIS: " << +ase->cis_conn_hdl
+ << " to stream. Allocation: "
+ << +ase->codec_config.audio_channel_allocation
+ << " sample_freq: " << +stream_conf->source_sample_frequency_hz
+ << " frame_duration: " << +stream_conf->source_frame_duration_us
+ << " octects per frame: "
+ << +stream_conf->source_octets_per_codec_frame;
+
+ return stream_conf;
+ }
+
+ void StartReceivingAudio(int group_id) {
+ LOG(INFO) << __func__;
+
+ LeAudioDeviceGroup* group = aseGroups_.FindById(group_id);
+
+ auto* stream_conf = GetStreamSourceConfiguration(group);
+ if (!stream_conf) {
+ LOG(WARNING) << " Could not get source configuration for group "
+ << active_group_id_ << " probably microphone not configured";
+ return;
+ }
+
+ Lc3Config lc3Config(
+ current_sink_codec_config.sample_rate,
+ Lc3ConfigFrameDuration(current_sink_codec_config.data_interval_us), 1);
+
+ lc3_decoder = new Lc3Decoder(lc3Config);
+
+ uint16_t remote_delay_ms =
+ group->GetRemoteDelay(le_audio::types::kLeAudioDirectionSource);
+
+ LeAudioClientAudioSink::UpdateRemoteDelay(remote_delay_ms);
+ LeAudioClientAudioSink::ConfirmStreamingRequest();
+ }
+
+ void SuspendAudio(void) {
+ audio_sink_ready_to_receive = false;
+ audio_source_ready_to_send = false;
+
+ if (lc3_encoder) {
+ delete lc3_encoder;
+ lc3_encoder = nullptr;
+ }
+
+ if (lc3_decoder) {
+ LOG(INFO) << __func__ << " stopping sink";
+
+ delete lc3_decoder;
+ lc3_decoder = nullptr;
+ }
+ }
+
+ void StopAudio(void) {
+ SuspendAudio();
+ ClientAudioIntefraceRelease();
+ }
+
+ void Dump(int fd) {
+ dprintf(fd, " Active group: %d\n", active_group_id_);
+ dprintf(fd, " LE Audio Groups:\n");
+ aseGroups_.Dump(fd);
+ dprintf(fd, " Not grouped devices:\n");
+ leAudioDevices_.Dump(fd, bluetooth::groups::kGroupUnknown);
+ }
+
+ void Cleanup(void) {
+ leAudioDevices_.Cleanup();
+ aseGroups_.Cleanup();
+ StopAudio();
+ if (gatt_if_) BTA_GATTC_AppDeregister(gatt_if_);
+ }
+
+ void UpdateCurrentHalSessions(int group_id, LeAudioContextType context_type) {
+ if (group_id == bluetooth::groups::kGroupUnknown) {
+ LOG(WARNING) << ", cannot start straming if no active group set";
+ return;
+ }
+
+ auto group = aseGroups_.FindById(group_id);
+ if (!group) {
+ LOG(ERROR) << __func__
+ << ", Invalid group: " << static_cast<int>(group_id);
+ return;
+ }
+
+ std::optional<LeAudioCodecConfiguration> source_configuration =
+ group->GetCodecConfigurationByDirection(
+ context_type, le_audio::types::kLeAudioDirectionSink);
+ std::optional<LeAudioCodecConfiguration> sink_configuration =
+ group->GetCodecConfigurationByDirection(
+ context_type, le_audio::types::kLeAudioDirectionSource);
+
+ if (source_configuration) {
+ /* Stream configuration differs from previous one */
+ if (!current_source_codec_config.IsInvalid() &&
+ (*source_configuration != current_source_codec_config))
+ LeAudioClientAudioSource::Stop();
+
+ current_source_codec_config = *source_configuration;
+
+ LeAudioClientAudioSource::Start(current_source_codec_config,
+ audioSinkReceiver);
+ } else {
+ if (!current_source_codec_config.IsInvalid()) {
+ LeAudioClientAudioSource::Stop();
+ current_source_codec_config = {0, 0, 0, 0};
+ }
+
+ LOG(INFO) << __func__
+ << ", group does not supports source direction for"
+ " context: "
+ << static_cast<int>(context_type);
+ }
+
+ if (sink_configuration) {
+ /* Stream configuration differs from previous one */
+ if (!current_sink_codec_config.IsInvalid() &&
+ (*sink_configuration != current_sink_codec_config))
+ LeAudioClientAudioSink::Stop();
+
+ current_sink_codec_config = *sink_configuration;
+
+ LeAudioClientAudioSink::Start(current_sink_codec_config,
+ audioSourceReceiver);
+ } else {
+ if (!current_sink_codec_config.IsInvalid()) {
+ LeAudioClientAudioSink::Stop();
+ current_sink_codec_config = {0, 0, 0, 0};
+ }
+
+ LOG(INFO) << __func__
+ << ", group does not supports sink direction for"
+ " context: "
+ << static_cast<int>(context_type);
+ }
+ }
+
+ void OnAudioResume() {
+ if (active_group_id_ == bluetooth::groups::kGroupUnknown) {
+ LOG(WARNING) << ", cannot start straming if no active group set";
+ return;
+ }
+
+ auto group = aseGroups_.FindById(active_group_id_);
+ if (!group) {
+ LOG(ERROR) << __func__
+ << ", Invalid group: " << static_cast<int>(active_group_id_);
+ return;
+ }
+
+ std::optional<LeAudioCodecConfiguration> source_configuration =
+ group->GetCodecConfigurationByDirection(
+ current_context_type_, le_audio::types::kLeAudioDirectionSink);
+ std::optional<LeAudioCodecConfiguration> sink_configuration =
+ group->GetCodecConfigurationByDirection(
+ current_context_type_, le_audio::types::kLeAudioDirectionSource);
+
+ /* Check if Bluetooth audio HAL session requires reconfiguration */
+ bool sessions_requires_update =
+ (((source_configuration &&
+ (*source_configuration != current_source_codec_config)) ||
+ (!source_configuration &&
+ !current_source_codec_config.IsInvalid()))) ||
+ ((sink_configuration &&
+ (*sink_configuration != current_sink_codec_config)) ||
+ (!sink_configuration && !current_sink_codec_config.IsInvalid()));
+ if (sessions_requires_update) {
+ CancelStreamingRequest();
+ do_in_main_thread(FROM_HERE,
+ base::Bind(&LeAudioClientImpl::UpdateCurrentHalSessions,
+ base::Unretained(instance), active_group_id_,
+ current_context_type_));
+ return;
+ }
+
+ /* TODO check if group already started streaming */
+
+ GroupStream(active_group_id_, static_cast<uint16_t>(current_context_type_));
+ }
+
+ void OnAudioSuspend() {
+ if (active_group_id_ == bluetooth::groups::kGroupUnknown) {
+ LOG(WARNING) << ", there is no longer active group";
+ return;
+ }
+
+ GroupStop(active_group_id_);
+ }
+
+ void OnAudioSinkSuspend() {
+ LOG(INFO) << __func__;
+
+ if (!audio_source_ready_to_send) return;
+
+ audio_source_ready_to_send = false;
+
+ /* Last suspends group - triggers group stop */
+ if (!audio_sink_ready_to_receive && !audio_source_ready_to_send)
+ OnAudioSuspend();
+ }
+
+ void OnAudioSinkResume() {
+ LOG(INFO) << __func__;
+
+ auto group = aseGroups_.FindById(active_group_id_);
+ if (!group) {
+ LOG(ERROR) << __func__
+ << ", Invalid group: " << static_cast<int>(active_group_id_);
+ return;
+ }
+
+ /* Check if the device resume is expected */
+ if (!group->GetCodecConfigurationByDirection(
+ current_context_type_, le_audio::types::kLeAudioDirectionSink)) {
+ LOG(ERROR) << __func__ << ", invalid resume request for context type: "
+ << loghex(static_cast<int>(current_context_type_));
+ LeAudioClientAudioSource::CancelStreamingRequest();
+ return;
+ }
+
+ /* First resume request from sink/source triggers group start */
+ if (!audio_sink_ready_to_receive && !audio_source_ready_to_send) {
+ audio_source_ready_to_send = true;
+ OnAudioResume();
+
+ return;
+ }
+
+ if (audio_sink_ready_to_receive) {
+ audio_source_ready_to_send = true;
+ /* If signalling part is completed trigger start reveivin audio here,
+ * otherwise it'll be called on group streaming state callback
+ */
+ if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING)
+ StartSendingAudio(active_group_id_);
+ }
+ }
+
+ void OnAudioSourceSuspend() {
+ LOG(INFO) << __func__;
+
+ if (!audio_sink_ready_to_receive) return;
+
+ audio_sink_ready_to_receive = false;
+
+ /* Last suspends group - triggers group stop */
+ if (!audio_sink_ready_to_receive && !audio_source_ready_to_send)
+ OnAudioSuspend();
+ }
+
+ void OnAudioSourceResume() {
+ LOG(INFO) << __func__;
+
+ auto group = aseGroups_.FindById(active_group_id_);
+ if (!group) {
+ LOG(ERROR) << __func__
+ << ", Invalid group: " << static_cast<int>(active_group_id_);
+ return;
+ }
+
+ /* Check if the device resume is expected */
+ if (!group->GetCodecConfigurationByDirection(
+ current_context_type_, le_audio::types::kLeAudioDirectionSource)) {
+ LOG(ERROR) << __func__ << ", invalid resume request for context type: "
+ << loghex(static_cast<int>(current_context_type_));
+ LeAudioClientAudioSink::CancelStreamingRequest();
+ return;
+ }
+
+ /* First resume request from sink/source triggers group start */
+ if (!audio_sink_ready_to_receive && !audio_source_ready_to_send) {
+ OnAudioResume();
+ audio_sink_ready_to_receive = true;
+
+ return;
+ }
+
+ if (audio_source_ready_to_send) {
+ audio_sink_ready_to_receive = true;
+ /* If signalling part is completed trigger start reveivin audio here,
+ * otherwise it'll be called on group streaming state callback
+ */
+ if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING)
+ StartReceivingAudio(active_group_id_);
+ }
+ }
+
+ void OnAudioMetadataUpdate(audio_usage_t usage,
+ audio_content_type_t content_type) {
+ LOG(INFO) << __func__ << ", content_type = "
+ << audio_content_type_to_string(content_type)
+ << ", usage = " << audio_usage_to_string(usage);
+
+ LeAudioContextType new_context = LeAudioContextType::RFU;
+
+ switch (content_type) {
+ case AUDIO_CONTENT_TYPE_SPEECH:
+ new_context = LeAudioContextType::CONVERSATIONAL;
+ break;
+ case AUDIO_CONTENT_TYPE_MUSIC:
+ case AUDIO_CONTENT_TYPE_MOVIE:
+ case AUDIO_CONTENT_TYPE_SONIFICATION:
+ new_context = LeAudioContextType::MEDIA;
+ break;
+ default:
+ break;
+ }
+
+ /* Context is not clear, consider also usage of stream */
+ if (new_context == LeAudioContextType::RFU) {
+ switch (usage) {
+ case AUDIO_USAGE_VOICE_COMMUNICATION:
+ new_context = LeAudioContextType::CONVERSATIONAL;
+ break;
+ case AUDIO_USAGE_GAME:
+ new_context = LeAudioContextType::GAME;
+ break;
+ case AUDIO_USAGE_NOTIFICATION:
+ new_context = LeAudioContextType::NOTIFICATIONS;
+ break;
+ case AUDIO_USAGE_NOTIFICATION_TELEPHONY_RINGTONE:
+ new_context = LeAudioContextType::RINGTONE;
+ break;
+ case AUDIO_USAGE_ALARM:
+ new_context = LeAudioContextType::ALERTS;
+ break;
+ case AUDIO_USAGE_EMERGENCY:
+ new_context = LeAudioContextType::EMERGENCYALARM;
+ break;
+ default:
+ new_context = LeAudioContextType::MEDIA;
+ break;
+ }
+ }
+
+ auto group = aseGroups_.FindById(active_group_id_);
+ if (!group) {
+ LOG(ERROR) << __func__
+ << ", Invalid group: " << static_cast<int>(active_group_id_);
+ return;
+ }
+
+ if ((new_context != current_context_type_) &&
+ (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING)) {
+ if (active_group_id_ == bluetooth::groups::kGroupUnknown) {
+ LOG(WARNING) << ", cannot start straming if no active group set";
+ return;
+ }
+
+ std::optional<LeAudioCodecConfiguration> source_configuration =
+ group->GetCodecConfigurationByDirection(
+ current_context_type_, le_audio::types::kLeAudioDirectionSink);
+ std::optional<LeAudioCodecConfiguration> sink_configuration =
+ group->GetCodecConfigurationByDirection(
+ current_context_type_, le_audio::types::kLeAudioDirectionSource);
+
+ if (source_configuration &&
+ (*source_configuration != current_source_codec_config))
+ return;
+ if (sink_configuration &&
+ (*sink_configuration != current_sink_codec_config))
+ return;
+
+ /* Configuration is the same for new context, just will do update
+ * metadata of stream
+ */
+ GroupStream(active_group_id_, static_cast<uint16_t>(new_context));
+ }
+
+ current_context_type_ = new_context;
+ }
+
+ static void OnGattReadRspStatic(uint16_t conn_id, tGATT_STATUS status,
+ uint16_t hdl, uint16_t len, uint8_t* value,
+ void* data) {
+ if (!instance) return;
+
+ if (status == GATT_SUCCESS) {
+ instance->LeAudioCharValueHandle(conn_id, hdl, len,
+ static_cast<uint8_t*>(value));
+ }
+
+ /* We use data to keep notify connected flag. */
+ if (data && !!PTR_TO_INT(data)) {
+ LeAudioDevice* leAudioDevice =
+ instance->leAudioDevices_.FindByConnId(conn_id);
+ leAudioDevice->notify_connected_after_read_ = false;
+ instance->connectionReady(leAudioDevice);
+ }
+ }
+
+ void IsoCigEventsCb(uint16_t event_type, void* data) {
+ switch (event_type) {
+ case bluetooth::hci::iso_manager::kIsoEventCigOnCreateCmpl: {
+ auto* evt = static_cast<cig_create_cmpl_evt*>(data);
+ LeAudioDeviceGroup* group = aseGroups_.FindById(evt->cig_id);
+ groupStateMachine_->ProcessHciNotifOnCigCreate(
+ group, evt->status, evt->cig_id, evt->conn_handles);
+ } break;
+ case bluetooth::hci::iso_manager::kIsoEventCigOnRemoveCmpl: {
+ auto* evt = static_cast<cig_remove_cmpl_evt*>(data);
+ LeAudioDeviceGroup* group = aseGroups_.FindById(evt->cig_id);
+ groupStateMachine_->ProcessHciNotifOnCigRemove(evt->status, group);
+ } break;
+ default:
+ LOG(ERROR) << __func__ << " Invalid event " << int{event_type};
+ }
+ }
+
+ void IsoCisEventsCb(uint16_t event_type, void* data) {
+ switch (event_type) {
+ case bluetooth::hci::iso_manager::kIsoEventCisDataAvailable: {
+ auto* event =
+ static_cast<bluetooth::hci::iso_manager::cis_data_evt*>(data);
+
+ if (!audio_sink_ready_to_receive) break;
+
+ SendAudioData(event->p_msg->data + event->p_msg->offset,
+ event->p_msg->len - event->p_msg->offset);
+ } break;
+ case bluetooth::hci::iso_manager::kIsoEventCisEstablishCmpl: {
+ auto* event =
+ static_cast<bluetooth::hci::iso_manager::cis_establish_cmpl_evt*>(
+ data);
+
+ LeAudioDevice* leAudioDevice =
+ leAudioDevices_.FindByCisConnHdl(event->cis_conn_hdl);
+ if (!leAudioDevice) {
+ LOG(ERROR) << __func__ << ", no bonded Le Audio Device with CIS: "
+ << +event->cis_conn_hdl;
+ break;
+ }
+ LeAudioDeviceGroup* group =
+ aseGroups_.FindById(leAudioDevice->group_id_);
+
+ if (event->max_pdu_mtos > 0)
+ group->SetTransportLatency(le_audio::types::kLeAudioDirectionSink,
+ event->trans_lat_mtos);
+ if (event->max_pdu_stom > 0)
+ group->SetTransportLatency(le_audio::types::kLeAudioDirectionSource,
+ event->trans_lat_stom);
+
+ groupStateMachine_->ProcessHciNotifCisEstablished(group, leAudioDevice,
+ event);
+ } break;
+ case bluetooth::hci::iso_manager::kIsoEventCisDisconnected: {
+ auto* event =
+ static_cast<bluetooth::hci::iso_manager::cis_disconnected_evt*>(
+ data);
+
+ LeAudioDevice* leAudioDevice =
+ leAudioDevices_.FindByCisConnHdl(event->cis_conn_hdl);
+ if (!leAudioDevice) {
+ LOG(ERROR) << __func__ << ", no bonded Le Audio Device with CIS: "
+ << +event->cis_conn_hdl;
+ break;
+ }
+ LeAudioDeviceGroup* group =
+ aseGroups_.FindById(leAudioDevice->group_id_);
+
+ groupStateMachine_->ProcessHciNotifCisDisconnected(group, leAudioDevice,
+ event);
+ } break;
+ default:
+ LOG(INFO) << ", Not handeled ISO event";
+ break;
+ }
+ }
+
+ void IsoSetupIsoDataPathCb(uint8_t status, uint16_t conn_handle,
+ uint8_t /* cig_id */) {
+ LeAudioDevice* leAudioDevice =
+ leAudioDevices_.FindByCisConnHdl(conn_handle);
+ LeAudioDeviceGroup* group = aseGroups_.FindById(leAudioDevice->group_id_);
+
+ instance->groupStateMachine_->ProcessHciNotifSetupIsoDataPath(
+ group, leAudioDevice, status, conn_handle);
+ }
+
+ void IsoRemoveIsoDataPathCb(uint8_t status, uint16_t conn_handle,
+ uint8_t /* cig_id */) {
+ LeAudioDevice* leAudioDevice =
+ leAudioDevices_.FindByCisConnHdl(conn_handle);
+ LeAudioDeviceGroup* group = aseGroups_.FindById(leAudioDevice->group_id_);
+
+ instance->groupStateMachine_->ProcessHciNotifRemoveIsoDataPath(
+ group, leAudioDevice, status, conn_handle);
+ }
+
+ void IsoLinkQualityReadCb(
+ uint8_t conn_handle, uint8_t cig_id, uint32_t txUnackedPackets,
+ uint32_t txFlushedPackets, uint32_t txLastSubeventPackets,
+ uint32_t retransmittedPackets, uint32_t crcErrorPackets,
+ uint32_t rxUnreceivedPackets, uint32_t duplicatePackets) {
+ LeAudioDevice* leAudioDevice =
+ leAudioDevices_.FindByCisConnHdl(conn_handle);
+ if (!leAudioDevice) {
+ LOG(WARNING) << __func__ << ", device under connection handle: "
+ << loghex(conn_handle)
+ << ", has been disconnecected in meantime";
+ return;
+ }
+ LeAudioDeviceGroup* group = aseGroups_.FindById(leAudioDevice->group_id_);
+
+ instance->groupStateMachine_->ProcessHciNotifIsoLinkQualityRead(
+ group, leAudioDevice, conn_handle, txUnackedPackets, txFlushedPackets,
+ txLastSubeventPackets, retransmittedPackets, crcErrorPackets,
+ rxUnreceivedPackets, duplicatePackets);
+ }
+
+ void StatusReportCb(int group_id, GroupStreamStatus status) {
+ switch (status) {
+ case GroupStreamStatus::STREAMING:
+ stream_request_started_ = false;
+ if (audio_source_ready_to_send) StartSendingAudio(active_group_id_);
+ if (audio_sink_ready_to_receive) StartReceivingAudio(active_group_id_);
+ break;
+ case GroupStreamStatus::SUSPENDED:
+ /** Stop Audio but don't release all the Audio resources */
+ SuspendAudio();
+ break;
+ case GroupStreamStatus::IDLE:
+ if (stream_request_started_) {
+ stream_request_started_ = false;
+ CancelStreamingRequest();
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ private:
+ tGATT_IF gatt_if_;
+ bluetooth::le_audio::LeAudioClientCallbacks* callbacks_;
+ LeAudioDevices leAudioDevices_;
+ LeAudioDeviceGroups aseGroups_;
+ LeAudioGroupStateMachine* groupStateMachine_;
+ int active_group_id_;
+ bool stream_request_started_;
+ LeAudioContextType current_context_type_;
+
+ bool audio_sink_ready_to_receive;
+ bool audio_source_ready_to_send;
+
+ LeAudioCodecConfiguration current_source_codec_config;
+ LeAudioCodecConfiguration current_sink_codec_config;
+ Lc3Encoder* lc3_encoder;
+ Lc3Decoder* lc3_decoder;
+ std::vector<uint8_t> encoded_data;
+ const void* audio_source_instance_;
+ const void* audio_sink_instance_;
+
+ void ClientAudioIntefraceRelease() {
+ if (audio_source_instance_) {
+ LeAudioClientAudioSource::Stop();
+ LeAudioClientAudioSource::Release(audio_source_instance_);
+ audio_source_instance_ = nullptr;
+ }
+
+ if (audio_sink_instance_) {
+ LeAudioClientAudioSink::Stop();
+ LeAudioClientAudioSink::Release(audio_sink_instance_);
+ audio_sink_instance_ = nullptr;
+ }
+ }
+};
+
+/* This is a generic callback method for gatt client which handles every client
+ * application events.
+ */
+void le_audio_gattc_callback(tBTA_GATTC_EVT event, tBTA_GATTC* p_data) {
+ if (!p_data || !instance) return;
+
+ DLOG(INFO) << __func__ << " event = " << +event;
+
+ switch (event) {
+ case BTA_GATTC_DEREG_EVT:
+ break;
+
+ case BTA_GATTC_NOTIF_EVT:
+ instance->LeAudioCharValueHandle(
+ p_data->notify.conn_id, p_data->notify.handle, p_data->notify.len,
+ static_cast<uint8_t*>(p_data->notify.value));
+
+ if (!p_data->notify.is_notify)
+ BTA_GATTC_SendIndConfirm(p_data->notify.conn_id, p_data->notify.handle);
+
+ break;
+
+ case BTA_GATTC_OPEN_EVT:
+ instance->OnGattConnected(p_data->open.status, p_data->open.conn_id,
+ p_data->open.client_if, p_data->open.remote_bda,
+ p_data->open.transport, p_data->open.mtu);
+ break;
+
+ case BTA_GATTC_ENC_CMPL_CB_EVT:
+ instance->OnEncryptionComplete(p_data->enc_cmpl.remote_bda, BTM_SUCCESS);
+ break;
+
+ case BTA_GATTC_CLOSE_EVT:
+ instance->OnGattDisconnected(
+ p_data->close.conn_id, p_data->close.client_if,
+ p_data->close.remote_bda, p_data->close.reason);
+ break;
+
+ case BTA_GATTC_SEARCH_CMPL_EVT:
+ instance->OnServiceSearchComplete(p_data->search_cmpl.conn_id,
+ p_data->search_cmpl.status);
+ break;
+
+ case BTA_GATTC_SRVC_DISC_DONE_EVT:
+ instance->OnGattServiceDiscoveryDone(p_data->service_changed.remote_bda);
+ break;
+
+ case BTA_GATTC_SRVC_CHG_EVT:
+ instance->OnServiceChangeEvent(p_data->remote_bda);
+ break;
+ case BTA_GATTC_CFG_MTU_EVT:
+ break;
+
+ default:
+ break;
+ }
+}
+
+class LeAudioStateMachineHciCallbacksImpl : public CigCallbacks {
+ public:
+ void OnCigEvent(uint8_t event, void* data) override {
+ if (instance) instance->IsoCigEventsCb(event, data);
+ }
+
+ void OnCisEvent(uint8_t event, void* data) override {
+ if (instance) instance->IsoCisEventsCb(event, data);
+ }
+
+ void OnSetupIsoDataPath(uint8_t status, uint16_t conn_handle,
+ uint8_t cig_id) override {
+ if (instance) instance->IsoSetupIsoDataPathCb(status, conn_handle, cig_id);
+ }
+
+ void OnRemoveIsoDataPath(uint8_t status, uint16_t conn_handle,
+ uint8_t cig_id) override {
+ if (instance) instance->IsoRemoveIsoDataPathCb(status, conn_handle, cig_id);
+ }
+
+ void OnIsoLinkQualityRead(
+ uint8_t conn_handle, uint8_t cig_id, uint32_t txUnackedPackets,
+ uint32_t txFlushedPackets, uint32_t txLastSubeventPackets,
+ uint32_t retransmittedPackets, uint32_t crcErrorPackets,
+ uint32_t rxUnreceivedPackets, uint32_t duplicatePackets) {
+ if (instance)
+ instance->IsoLinkQualityReadCb(conn_handle, cig_id, txUnackedPackets,
+ txFlushedPackets, txLastSubeventPackets,
+ retransmittedPackets, crcErrorPackets,
+ rxUnreceivedPackets, duplicatePackets);
+ }
+};
+
+LeAudioStateMachineHciCallbacksImpl stateMachineHciCallbacksImpl;
+
+class CallbacksImpl : public LeAudioGroupStateMachine::Callbacks {
+ public:
+ void StatusReportCb(int group_id, GroupStreamStatus status) override {
+ if (instance) instance->StatusReportCb(group_id, status);
+ }
+
+ void OnStateTransitionTimeout(int group_id) override {
+ if (instance) instance->OnLeAudioDeviceSetStateTimeout(group_id);
+ }
+};
+
+CallbacksImpl stateMachineCallbacksImpl;
+
+class LeAudioClientAudioSinkReceiverImpl
+ : public LeAudioClientAudioSinkReceiver {
+ public:
+ void OnAudioDataReady(const std::vector<uint8_t>& data) override {
+ if (instance) instance->OnAudioDataReady(data);
+ }
+ void OnAudioSuspend(std::promise<void> do_suspend_promise) override {
+ if (instance) instance->OnAudioSinkSuspend();
+ do_suspend_promise.set_value();
+ }
+
+ void OnAudioResume(std::promise<void> do_resume_promise) override {
+ if (instance) instance->OnAudioSinkResume();
+ do_resume_promise.set_value();
+ }
+
+ void OnAudioMetadataUpdate(std::promise<void> do_metadata_update_promise,
+ audio_usage_t usage,
+ audio_content_type_t content_type) override {
+ if (instance) instance->OnAudioMetadataUpdate(usage, content_type);
+ do_metadata_update_promise.set_value();
+ }
+};
+
+class LeAudioClientAudioSourceReceiverImpl
+ : public LeAudioClientAudioSourceReceiver {
+ public:
+ void OnAudioSuspend(std::promise<void> do_suspend_promise) override {
+ if (instance) instance->OnAudioSourceSuspend();
+ do_suspend_promise.set_value();
+ }
+ void OnAudioResume(std::promise<void> do_resume_promise) override {
+ if (instance) instance->OnAudioSourceResume();
+ do_resume_promise.set_value();
+ }
+};
+
+LeAudioClientAudioSinkReceiverImpl audioSinkReceiverImpl;
+LeAudioClientAudioSourceReceiverImpl audioSourceReceiverImpl;
+
+class DeviceGroupsCallbacksImpl : public DeviceGroupsCallbacks {
+ public:
+ void OnGroupAdded(const RawAddress& address, const bluetooth::Uuid& uuid,
+ int group_id) override {
+ if (instance) instance->OnGroupAddedCb(address, uuid, group_id);
+ }
+ void OnGroupMemberAdded(const RawAddress& address, int group_id) override {
+ if (instance) instance->OnGroupMemberAddedCb(address, group_id);
+ }
+ void OnGroupMemberRemoved(const RawAddress& address, int group_id) override {
+ if (instance) instance->OnGroupMemberRemovedCb(address, group_id);
+ }
+ void OnGroupRemoved(const bluetooth::Uuid& uuid, int group_id) {
+ /* to implement if needed */
+ }
+ void OnGroupAddFromStorage(const RawAddress& address,
+ const bluetooth::Uuid& uuid, int group_id) {
+ /* to implement if needed */
+ }
+};
+
+class DeviceGroupsCallbacksImpl;
+DeviceGroupsCallbacksImpl deviceGroupsCallbacksImpl;
+
+} // namespace
+
+void LeAudioClient::AddFromStorage(const RawAddress& addr, bool autoconnect) {
+ if (!instance) {
+ LOG(ERROR) << "Not initialized yet";
+ return;
+ }
+
+ instance->AddFromStorage(addr, autoconnect);
+}
+
+bool LeAudioClient::IsLeAudioClientRunning(void) { return instance != nullptr; }
+
+LeAudioClient* LeAudioClient::Get() {
+ CHECK(instance);
+ return instance;
+}
+
+/* Initializer of main le audio implementation class and its instance */
+void LeAudioClient::Initialize(
+ bluetooth::le_audio::LeAudioClientCallbacks* callbacks_,
+ base::Closure initCb, base::Callback<bool()> hal_2_1_verifier) {
+ if (instance) {
+ LOG(ERROR) << "Already initialized";
+ return;
+ }
+
+ if (!controller_get_interface()
+ ->supports_ble_connected_isochronous_stream_central() &&
+ !controller_get_interface()
+ ->supports_ble_connected_isochronous_stream_peripheral()) {
+ LOG(ERROR) << "Controller reports no ISO support."
+ " LeAudioClient Init aborted.";
+ return;
+ }
+
+ LOG_ASSERT(std::move(hal_2_1_verifier).Run())
+ << __func__
+ << ", LE Audio Client requires Bluetooth Audio HAL V2.1 at least. Either "
+ "disable LE Audio Profile, or update your HAL";
+
+ IsoManager::GetInstance()->Start();
+
+ audioSinkReceiver = &audioSinkReceiverImpl;
+ audioSourceReceiver = &audioSourceReceiverImpl;
+ stateMachineHciCallbacks = &stateMachineHciCallbacksImpl;
+ stateMachineCallbacks = &stateMachineCallbacksImpl;
+ device_group_callbacks = &deviceGroupsCallbacksImpl;
+ instance = new LeAudioClientImpl(callbacks_, stateMachineCallbacks, initCb);
+
+ IsoManager::GetInstance()->RegisterCigCallbacks(stateMachineHciCallbacks);
+}
+
+void LeAudioClient::DebugDump(int fd) {
+ dprintf(fd, "LeAudio Manager: \n");
+ if (instance)
+ instance->Dump(fd);
+ else
+ dprintf(fd, " Not initialized \n");
+
+ LeAudioClientAudioSource::DebugDump(fd);
+ LeAudioClientAudioSink::DebugDump(fd);
+ dprintf(fd, "\n");
+}
+
+void LeAudioClient::Cleanup(void) {
+ if (!instance) {
+ LOG(ERROR) << "Not initialized";
+ return;
+ }
+
+ LeAudioClientImpl* ptr = instance;
+ instance = nullptr;
+ ptr->Cleanup();
+ delete ptr;
+
+ LeAudioGroupStateMachine::Cleanup();
+ IsoManager::GetInstance()->Stop();
+}
diff --git a/bta/le_audio/client_audio.cc b/bta/le_audio/client_audio.cc
new file mode 100644
index 000000000..e75835037
--- /dev/null
+++ b/bta/le_audio/client_audio.cc
@@ -0,0 +1,573 @@
+/******************************************************************************
+ *
+ * Copyright 2019 HIMSA II K/S - www.himsa.com. Represented by EHIMA -
+ * www.ehima.com
+ *
+ * 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.
+ *
+ ******************************************************************************/
+
+#include "client_audio.h"
+
+#include "audio_hal_interface/le_audio_software.h"
+#include "btu.h"
+#include "common/repeating_timer.h"
+#include "common/time_util.h"
+#include "osi/include/wakelock.h"
+
+using bluetooth::audio::le_audio::LeAudioClientInterface;
+
+namespace {
+LeAudioCodecConfiguration source_codec_config;
+bluetooth::common::RepeatingTimer audio_timer;
+LeAudioClientInterface* leAudioClientInterface = nullptr;
+LeAudioClientInterface::Sink* sinkClientInterface = nullptr;
+LeAudioClientInterface::Source* sourceClientInterface = nullptr;
+LeAudioClientAudioSinkReceiver* localAudioSinkReceiver = nullptr;
+LeAudioClientAudioSourceReceiver* localAudioSourceReceiver = nullptr;
+
+enum {
+ HAL_UNINITIALIZED,
+ HAL_STOPPED,
+ HAL_STARTED,
+} le_audio_sink_hal_state,
+ le_audio_source_hal_state;
+
+struct AudioHalStats {
+ size_t media_read_total_underflow_bytes;
+ size_t media_read_total_underflow_count;
+ uint64_t media_read_last_underflow_us;
+
+ AudioHalStats() { Reset(); }
+
+ void Reset() {
+ media_read_total_underflow_bytes = 0;
+ media_read_total_underflow_count = 0;
+ media_read_last_underflow_us = 0;
+ }
+};
+
+AudioHalStats stats;
+
+bool le_audio_sink_on_resume_req(bool start_media_task);
+bool le_audio_sink_on_suspend_req();
+
+void send_audio_data() {
+ uint32_t bytes_per_tick =
+ (source_codec_config.num_channels * source_codec_config.sample_rate *
+ source_codec_config.data_interval_us / 1000 *
+ (source_codec_config.bits_per_sample / 8)) /
+ 1000;
+
+ std::vector<uint8_t> data(bytes_per_tick);
+
+ uint32_t bytes_read = 0;
+ if (sinkClientInterface != nullptr) {
+ bytes_read = sinkClientInterface->Read(data.data(), bytes_per_tick);
+ } else {
+ LOG(ERROR) << __func__ << ", no LE Audio sink client interface - aborting.";
+ return;
+ }
+
+ // LOG(INFO) << __func__ << ", bytes_read: " << static_cast<int>(bytes_read)
+ // << ", bytes_per_tick: " << static_cast<int>(bytes_per_tick);
+
+ if (bytes_read < bytes_per_tick) {
+ stats.media_read_total_underflow_bytes += bytes_per_tick - bytes_read;
+ stats.media_read_total_underflow_count++;
+ stats.media_read_last_underflow_us =
+ bluetooth::common::time_get_os_boottime_us();
+ }
+
+ if (localAudioSinkReceiver != nullptr) {
+ localAudioSinkReceiver->OnAudioDataReady(data);
+ }
+}
+
+void start_audio_ticks() {
+ wakelock_acquire();
+ audio_timer.SchedulePeriodic(
+ get_main_thread()->GetWeakPtr(), FROM_HERE, base::Bind(&send_audio_data),
+ base::TimeDelta::FromMicroseconds(source_codec_config.data_interval_us));
+}
+
+void stop_audio_ticks() {
+ audio_timer.CancelAndWait();
+ wakelock_release();
+}
+
+bool le_audio_sink_on_resume_req(bool start_media_task) {
+ if (localAudioSinkReceiver != nullptr) {
+ // Call OnAudioResume and block till it returns.
+ std::promise<void> do_resume_promise;
+ std::future<void> do_resume_future = do_resume_promise.get_future();
+ bt_status_t status = do_in_main_thread(
+ FROM_HERE,
+ base::BindOnce(&LeAudioClientAudioSinkReceiver::OnAudioResume,
+ base::Unretained(localAudioSinkReceiver),
+ std::move(do_resume_promise)));
+ if (status == BT_STATUS_SUCCESS) {
+ do_resume_future.wait();
+ } else {
+ LOG(ERROR) << __func__
+ << ": LE_AUDIO_CTRL_CMD_START: do_in_main_thread err="
+ << status;
+ return false;
+ }
+ } else {
+ LOG(ERROR) << __func__
+ << ": LE_AUDIO_CTRL_CMD_START: audio sink receiver not started";
+ return false;
+ }
+
+ return true;
+}
+
+bool le_audio_source_on_resume_req(bool start_media_task) {
+ if (localAudioSourceReceiver != nullptr) {
+ // Call OnAudioResume and block till it returns.
+ std::promise<void> do_resume_promise;
+ std::future<void> do_resume_future = do_resume_promise.get_future();
+ bt_status_t status = do_in_main_thread(
+ FROM_HERE,
+ base::BindOnce(&LeAudioClientAudioSourceReceiver::OnAudioResume,
+ base::Unretained(localAudioSourceReceiver),
+ std::move(do_resume_promise)));
+ if (status == BT_STATUS_SUCCESS) {
+ do_resume_future.wait();
+ } else {
+ LOG(ERROR) << __func__
+ << ": LE_AUDIO_CTRL_CMD_START: do_in_main_thread err="
+ << status;
+ return false;
+ }
+ } else {
+ LOG(ERROR)
+ << __func__
+ << ": LE_AUDIO_CTRL_CMD_START: audio source receiver not started";
+ return false;
+ }
+
+ return true;
+}
+
+bool le_audio_sink_on_suspend_req() {
+ stop_audio_ticks();
+ if (localAudioSinkReceiver != nullptr) {
+ // Call OnAudioSuspend and block till it returns.
+ std::promise<void> do_suspend_promise;
+ std::future<void> do_suspend_future = do_suspend_promise.get_future();
+ bt_status_t status = do_in_main_thread(
+ FROM_HERE,
+ base::BindOnce(&LeAudioClientAudioSinkReceiver::OnAudioSuspend,
+ base::Unretained(localAudioSinkReceiver),
+ std::move(do_suspend_promise)));
+ if (status == BT_STATUS_SUCCESS) {
+ do_suspend_future.wait();
+ return true;
+ } else {
+ LOG(ERROR) << __func__
+ << ": LE_AUDIO_CTRL_CMD_SUSPEND: do_in_main_thread err="
+ << status;
+ }
+ } else {
+ LOG(ERROR) << __func__
+ << ": LE_AUDIO_CTRL_CMD_SUSPEND: audio receiver not started";
+ }
+ return false;
+}
+
+bool le_audio_source_on_suspend_req() {
+ if (localAudioSourceReceiver != nullptr) {
+ // Call OnAudioSuspend and block till it returns.
+ std::promise<void> do_suspend_promise;
+ std::future<void> do_suspend_future = do_suspend_promise.get_future();
+ bt_status_t status = do_in_main_thread(
+ FROM_HERE,
+ base::BindOnce(&LeAudioClientAudioSourceReceiver::OnAudioSuspend,
+ base::Unretained(localAudioSourceReceiver),
+ std::move(do_suspend_promise)));
+ if (status == BT_STATUS_SUCCESS) {
+ do_suspend_future.wait();
+ return true;
+ } else {
+ LOG(ERROR) << __func__
+ << ": LE_AUDIO_CTRL_CMD_SUSPEND: do_in_main_thread err="
+ << status;
+ }
+ } else {
+ LOG(ERROR) << __func__
+ << ": LE_AUDIO_CTRL_CMD_SUSPEND: audio receiver not started";
+ }
+ return false;
+}
+
+bool le_audio_sink_on_metadata_update_req(audio_usage_t usage,
+ audio_content_type_t content_type) {
+ if (localAudioSinkReceiver == nullptr) {
+ LOG(ERROR) << __func__ << ", audio receiver not started";
+ return false;
+ }
+
+ // Call OnAudioSuspend and block till it returns.
+ std::promise<void> do_update_metadata_promise;
+ std::future<void> do_update_metadata_future =
+ do_update_metadata_promise.get_future();
+ bt_status_t status = do_in_main_thread(
+ FROM_HERE,
+ base::BindOnce(&LeAudioClientAudioSinkReceiver::OnAudioMetadataUpdate,
+ base::Unretained(localAudioSinkReceiver),
+ std::move(do_update_metadata_promise), usage,
+ content_type));
+
+ if (status == BT_STATUS_SUCCESS) {
+ do_update_metadata_future.wait();
+ return true;
+ }
+
+ LOG(ERROR) << __func__ << ", do_in_main_thread err=" << status;
+
+ return false;
+}
+
+} // namespace
+
+bool LeAudioClientAudioSource::Start(
+ const LeAudioCodecConfiguration& codec_configuration,
+ LeAudioClientAudioSinkReceiver* audioReceiver) {
+ LOG(INFO) << __func__;
+
+ if (!sinkClientInterface) {
+ LOG(ERROR) << "sinkClientInterface is not Acquired!";
+ return false;
+ }
+
+ if (le_audio_sink_hal_state == HAL_STARTED) {
+ LOG(ERROR) << "LE audio device HAL is already in use!";
+ return false;
+ }
+
+ LOG(INFO) << __func__ << ": Le Audio Source Open, bits per sample: "
+ << int{codec_configuration.bits_per_sample}
+ << ", num channels: " << int{codec_configuration.num_channels}
+ << ", sample rate: " << codec_configuration.sample_rate
+ << ", data interval: " << codec_configuration.data_interval_us;
+
+ stats.Reset();
+
+ /* Global config for periodic audio data */
+ source_codec_config = codec_configuration;
+ LeAudioClientInterface::PcmParameters pcmParameters = {
+ .data_interval_us = codec_configuration.data_interval_us,
+ .sample_rate = codec_configuration.sample_rate,
+ .bits_per_sample = codec_configuration.bits_per_sample,
+ .channels_count = codec_configuration.num_channels};
+
+ sinkClientInterface->SetPcmParameters(pcmParameters);
+ sinkClientInterface->StartSession();
+
+ localAudioSinkReceiver = audioReceiver;
+ le_audio_sink_hal_state = HAL_STARTED;
+
+ return true;
+}
+
+void LeAudioClientAudioSource::Stop() {
+ LOG(INFO) << __func__;
+ if (!sinkClientInterface) {
+ LOG(ERROR) << __func__ << " sinkClientInterface stopped";
+ return;
+ }
+
+ if (le_audio_sink_hal_state != HAL_STARTED) {
+ LOG(ERROR) << "LE audio device HAL was not started!";
+ return;
+ }
+
+ LOG(INFO) << __func__ << ": Le Audio Source Close";
+
+ sinkClientInterface->StopSession();
+ le_audio_sink_hal_state = HAL_STOPPED;
+ localAudioSinkReceiver = nullptr;
+
+ stop_audio_ticks();
+}
+
+const void* LeAudioClientAudioSource::Acquire() {
+ LOG(INFO) << __func__;
+ if (sinkClientInterface != nullptr) {
+ LOG(WARNING) << __func__ << ", Sink client interface already initialized";
+ return nullptr;
+ }
+
+ /* Get pointer to singleton LE audio client interface */
+ if (leAudioClientInterface == nullptr) {
+ leAudioClientInterface = LeAudioClientInterface::Get();
+
+ if (leAudioClientInterface == nullptr) {
+ LOG(ERROR) << __func__ << ", can't get LE audio client interface";
+ return nullptr;
+ }
+ }
+
+ auto sink_stream_cb = bluetooth::audio::le_audio::StreamCallbacks{
+ .on_resume_ = le_audio_sink_on_resume_req,
+ .on_suspend_ = le_audio_sink_on_suspend_req,
+ .on_metadata_update_ = le_audio_sink_on_metadata_update_req,
+ };
+
+ sinkClientInterface =
+ leAudioClientInterface->GetSink(sink_stream_cb, get_main_thread());
+
+ if (sinkClientInterface == nullptr) {
+ LOG(ERROR) << __func__ << ", can't get LE audio sink client interface";
+ return nullptr;
+ }
+
+ le_audio_sink_hal_state = HAL_STOPPED;
+ return sinkClientInterface;
+}
+
+void LeAudioClientAudioSource::Release(const void* instance) {
+ LOG(INFO) << __func__;
+ if (sinkClientInterface != instance) {
+ LOG(WARNING) << "Trying to release not own session";
+ return;
+ }
+
+ if (le_audio_sink_hal_state == HAL_UNINITIALIZED) {
+ LOG(WARNING) << "LE audio device HAL is not running.";
+ return;
+ }
+
+ sinkClientInterface->Cleanup();
+ leAudioClientInterface->ReleaseSink(sinkClientInterface);
+ le_audio_sink_hal_state = HAL_UNINITIALIZED;
+ sinkClientInterface = nullptr;
+}
+
+void LeAudioClientAudioSource::ConfirmStreamingRequest() {
+ LOG(INFO) << __func__;
+ if ((sinkClientInterface == nullptr) ||
+ (le_audio_sink_hal_state != HAL_STARTED)) {
+ LOG(ERROR) << "LE audio device HAL was not started!";
+ return;
+ }
+
+ sinkClientInterface->ConfirmStreamingRequest();
+ LOG(INFO) << __func__ << ", start_audio_ticks";
+ start_audio_ticks();
+}
+
+void LeAudioClientAudioSource::CancelStreamingRequest() {
+ LOG(INFO) << __func__;
+ if ((sinkClientInterface == nullptr) ||
+ (le_audio_sink_hal_state != HAL_STARTED)) {
+ LOG(ERROR) << "LE audio device HAL was not started!";
+ return;
+ }
+
+ sinkClientInterface->CancelStreamingRequest();
+}
+
+void LeAudioClientAudioSource::UpdateRemoteDelay(uint16_t remote_delay_ms) {
+ LOG(INFO) << __func__;
+ if ((sinkClientInterface == nullptr) ||
+ (le_audio_sink_hal_state != HAL_STARTED)) {
+ LOG(ERROR) << "LE audio device HAL was not started!";
+ return;
+ }
+
+ sinkClientInterface->SetRemoteDelay(remote_delay_ms);
+}
+
+void LeAudioClientAudioSource::DebugDump(int fd) {
+ uint64_t now_us = bluetooth::common::time_get_os_boottime_us();
+ std::stringstream stream;
+ stream << " Le Audio Audio HAL:"
+ << "\n Counts (underflow) : "
+ << stats.media_read_total_underflow_count
+ << "\n Bytes (underflow) : "
+ << stats.media_read_total_underflow_bytes
+ << "\n Last update time ago in ms (underflow) : "
+ << (stats.media_read_last_underflow_us > 0
+ ? (unsigned long long)(now_us -
+ stats.media_read_last_underflow_us) /
+ 1000
+ : 0)
+ << std::endl;
+ dprintf(fd, "%s", stream.str().c_str());
+}
+
+bool LeAudioClientAudioSink::Start(
+ const LeAudioCodecConfiguration& codec_configuration,
+ LeAudioClientAudioSourceReceiver* audioReceiver) {
+ LOG(INFO) << __func__;
+ if (!sourceClientInterface) {
+ LOG(ERROR) << "sourceClientInterface is not Acquired!";
+ return false;
+ }
+
+ if (le_audio_source_hal_state == HAL_STARTED) {
+ LOG(ERROR) << "LE audio device HAL is already in use!";
+ return false;
+ }
+
+ LOG(INFO) << __func__ << ": Le Audio Sink Open, bit rate: "
+ << codec_configuration.bits_per_sample
+ << ", num channels: " << int{codec_configuration.num_channels}
+ << ", sample rate: " << codec_configuration.sample_rate
+ << ", data interval: " << codec_configuration.data_interval_us;
+
+ LeAudioClientInterface::PcmParameters pcmParameters = {
+ .data_interval_us = codec_configuration.data_interval_us,
+ .sample_rate = codec_configuration.sample_rate,
+ .bits_per_sample = codec_configuration.bits_per_sample,
+ .channels_count = codec_configuration.num_channels};
+
+ sourceClientInterface->SetPcmParameters(pcmParameters);
+ sourceClientInterface->StartSession();
+
+ localAudioSourceReceiver = audioReceiver;
+ le_audio_source_hal_state = HAL_STARTED;
+ return true;
+}
+
+void LeAudioClientAudioSink::Stop() {
+ LOG(INFO) << __func__;
+ if (!sourceClientInterface) {
+ LOG(ERROR) << __func__ << " sourceClientInterface stopped";
+ return;
+ }
+
+ if (le_audio_source_hal_state != HAL_STARTED) {
+ LOG(ERROR) << "LE audio device HAL was not started!";
+ return;
+ }
+
+ LOG(INFO) << __func__ << ": Le Audio Sink Close";
+
+ sourceClientInterface->StopSession();
+ le_audio_source_hal_state = HAL_STOPPED;
+ localAudioSourceReceiver = nullptr;
+}
+
+const void* LeAudioClientAudioSink::Acquire() {
+ LOG(INFO) << __func__;
+ if (sourceClientInterface != nullptr) {
+ LOG(WARNING) << __func__ << ", Source client interface already initialized";
+ return nullptr;
+ }
+
+ /* Get pointer to singleton LE audio client interface */
+ if (leAudioClientInterface == nullptr) {
+ leAudioClientInterface = LeAudioClientInterface::Get();
+
+ if (leAudioClientInterface == nullptr) {
+ LOG(ERROR) << __func__ << ", can't get LE audio client interface";
+ return nullptr;
+ }
+ }
+
+ auto source_stream_cb = bluetooth::audio::le_audio::StreamCallbacks{
+ .on_resume_ = le_audio_source_on_resume_req,
+ .on_suspend_ = le_audio_source_on_suspend_req,
+ };
+
+ sourceClientInterface =
+ leAudioClientInterface->GetSource(source_stream_cb, get_main_thread());
+
+ if (sourceClientInterface == nullptr) {
+ LOG(ERROR) << __func__ << ", can't get LE audio source client interface";
+ return nullptr;
+ }
+
+ le_audio_source_hal_state = HAL_STOPPED;
+ return sourceClientInterface;
+}
+
+void LeAudioClientAudioSink::Release(const void* instance) {
+ LOG(INFO) << __func__;
+ if (sourceClientInterface != instance) {
+ LOG(WARNING) << "Trying to release not own session";
+ return;
+ }
+
+ if (le_audio_source_hal_state == HAL_UNINITIALIZED) {
+ LOG(WARNING) << ", LE audio device source HAL is not running.";
+ return;
+ }
+
+ sourceClientInterface->Cleanup();
+ leAudioClientInterface->ReleaseSource(sourceClientInterface);
+ le_audio_source_hal_state = HAL_UNINITIALIZED;
+ sourceClientInterface = nullptr;
+}
+
+size_t LeAudioClientAudioSink::SendData(uint8_t* data, uint16_t size) {
+ size_t bytes_written;
+ if (!sourceClientInterface) {
+ LOG(ERROR) << "sourceClientInterface not initialized!";
+ return 0;
+ }
+
+ if (le_audio_source_hal_state != HAL_STARTED) {
+ LOG(ERROR) << "LE audio device HAL was not started!";
+ return 0;
+ }
+
+ /* TODO: What to do if not all data is written ? */
+ bytes_written = sourceClientInterface->Write(data, size);
+ if (bytes_written != size)
+ LOG(ERROR) << ", Not all data is written to source HAL. bytes written: "
+ << static_cast<int>(bytes_written)
+ << ", total: " << static_cast<int>(size);
+
+ return bytes_written;
+}
+
+void LeAudioClientAudioSink::ConfirmStreamingRequest() {
+ LOG(INFO) << __func__;
+ if ((sourceClientInterface == nullptr) ||
+ (le_audio_source_hal_state != HAL_STARTED)) {
+ LOG(ERROR) << "LE audio device HAL was not started!";
+ return;
+ }
+
+ sourceClientInterface->ConfirmStreamingRequest();
+}
+
+void LeAudioClientAudioSink::CancelStreamingRequest() {
+ LOG(INFO) << __func__;
+ if ((sourceClientInterface == nullptr) ||
+ (le_audio_source_hal_state != HAL_STARTED)) {
+ LOG(ERROR) << "LE audio device HAL was not started!";
+ return;
+ }
+
+ sourceClientInterface->CancelStreamingRequest();
+}
+
+void LeAudioClientAudioSink::UpdateRemoteDelay(uint16_t remote_delay_ms) {
+ if ((sourceClientInterface == nullptr) ||
+ (le_audio_source_hal_state != HAL_STARTED)) {
+ LOG(ERROR) << "LE audio device HAL was not started!";
+ return;
+ }
+
+ sourceClientInterface->SetRemoteDelay(remote_delay_ms);
+}
+
+void LeAudioClientAudioSink::DebugDump(int fd) {
+ /* TODO: Add some statistic for source client interface */
+}
diff --git a/bta/le_audio/client_audio.h b/bta/le_audio/client_audio.h
new file mode 100644
index 000000000..1bca6185f
--- /dev/null
+++ b/bta/le_audio/client_audio.h
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+#pragma once
+
+#include <future>
+
+#include "audio_hal_interface/le_audio_software.h"
+
+/* Implementations of Le Audio will also implement this interface */
+class LeAudioClientAudioSinkReceiver {
+ public:
+ virtual ~LeAudioClientAudioSinkReceiver() = default;
+ virtual void OnAudioDataReady(const std::vector<uint8_t>& data) = 0;
+ virtual void OnAudioSuspend(std::promise<void> do_suspend_promise) = 0;
+ virtual void OnAudioResume(std::promise<void> do_resume_promise) = 0;
+ virtual void OnAudioMetadataUpdate(
+ std::promise<void> do_update_metadata_promise, audio_usage_t usage,
+ audio_content_type_t content_type) = 0;
+};
+class LeAudioClientAudioSourceReceiver {
+ public:
+ virtual ~LeAudioClientAudioSourceReceiver() = default;
+ virtual void OnAudioSuspend(std::promise<void> do_suspend_promise) = 0;
+ virtual void OnAudioResume(std::promise<void> do_resume_promise) = 0;
+};
+
+/* Represents configuration of audio codec, as exchanged between le audio and
+ * phone.
+ * It can also be passed to the audio source to configure its parameters.
+ */
+struct LeAudioCodecConfiguration {
+ static constexpr uint8_t kChannelNumberMono =
+ bluetooth::audio::le_audio::kChannelNumberMono;
+ static constexpr uint8_t kChannelNumberStereo =
+ bluetooth::audio::le_audio::kChannelNumberStereo;
+
+ static constexpr uint32_t kSampleRate48000 =
+ bluetooth::audio::le_audio::kSampleRate48000;
+ static constexpr uint32_t kSampleRate44100 =
+ bluetooth::audio::le_audio::kSampleRate44100;
+ static constexpr uint32_t kSampleRate32000 =
+ bluetooth::audio::le_audio::kSampleRate32000;
+ static constexpr uint32_t kSampleRate24000 =
+ bluetooth::audio::le_audio::kSampleRate24000;
+ static constexpr uint32_t kSampleRate16000 =
+ bluetooth::audio::le_audio::kSampleRate16000;
+ static constexpr uint32_t kSampleRate8000 =
+ bluetooth::audio::le_audio::kSampleRate8000;
+
+ static constexpr uint8_t kBitsPerSample16 =
+ bluetooth::audio::le_audio::kBitsPerSample16;
+ static constexpr uint8_t kBitsPerSample24 =
+ bluetooth::audio::le_audio::kBitsPerSample24;
+ static constexpr uint8_t kBitsPerSample32 =
+ bluetooth::audio::le_audio::kBitsPerSample32;
+
+ static constexpr uint32_t kInterval7500Us = 7500;
+ static constexpr uint32_t kInterval10000Us = 10000;
+
+ /** number of channels */
+ uint8_t num_channels;
+
+ /** sampling rate that the codec expects to receive from audio framework */
+ uint32_t sample_rate;
+
+ /** bits per sample that codec expects to receive from audio framework */
+ uint8_t bits_per_sample;
+
+ /** Data interval determines how often we send samples to the remote. This
+ * should match how often we grab data from audio source, optionally we can
+ * grab data every 2 or 3 intervals, but this would increase latency.
+ *
+ * Value is provided in us.
+ */
+ uint32_t data_interval_us;
+
+ bool operator!=(const LeAudioCodecConfiguration& other) {
+ return !((num_channels == other.num_channels) &&
+ (sample_rate == other.sample_rate) &&
+ (bits_per_sample == other.bits_per_sample) &&
+ (data_interval_us == other.data_interval_us));
+ }
+
+ bool IsInvalid() {
+ return (num_channels == 0) || (sample_rate == 0) ||
+ (bits_per_sample == 0) || (data_interval_us == 0);
+ }
+};
+
+/* Represents source of audio for le audio client */
+class LeAudioClientAudioSource {
+ public:
+ static bool Start(const LeAudioCodecConfiguration& codecConfiguration,
+ LeAudioClientAudioSinkReceiver* audioReceiver);
+ static void Stop();
+ static const void* Acquire();
+ static void Release(const void* instance);
+ static void ConfirmStreamingRequest();
+ static void CancelStreamingRequest();
+ static void UpdateRemoteDelay(uint16_t remote_delay_ms);
+ static void DebugDump(int fd);
+};
+
+/* Represents audio sink for le audio client */
+class LeAudioClientAudioSink {
+ public:
+ static bool Start(const LeAudioCodecConfiguration& codecConfiguration,
+ LeAudioClientAudioSourceReceiver* audioReceiver);
+ static void Stop();
+ static const void* Acquire();
+ static void Release(const void* instance);
+ static size_t SendData(uint8_t* data, uint16_t size);
+ static void ConfirmStreamingRequest();
+ static void CancelStreamingRequest();
+ static void UpdateRemoteDelay(uint16_t remote_delay_ms);
+ static void DebugDump(int fd);
+};
diff --git a/bta/le_audio/client_audio_test.cc b/bta/le_audio/client_audio_test.cc
new file mode 100644
index 000000000..919f267ae
--- /dev/null
+++ b/bta/le_audio/client_audio_test.cc
@@ -0,0 +1,519 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+#include "client_audio.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <chrono>
+#include <future>
+
+#include "audio_hal_interface/le_audio_software.h"
+#include "base/bind_helpers.h"
+#include "common/message_loop_thread.h"
+#include "hardware/bluetooth.h"
+#include "osi/include/wakelock.h"
+
+using ::testing::_;
+using ::testing::Assign;
+using ::testing::AtLeast;
+using ::testing::DoAll;
+using ::testing::Invoke;
+using ::testing::Mock;
+using ::testing::Return;
+using ::testing::ReturnPointee;
+using ::testing::SaveArg;
+using std::chrono_literals::operator""ms;
+
+bluetooth::common::MessageLoopThread message_loop_thread("test message loop");
+bluetooth::common::MessageLoopThread* get_main_thread() {
+ return &message_loop_thread;
+}
+bt_status_t do_in_main_thread(const base::Location& from_here,
+ base::OnceClosure task) {
+ if (!message_loop_thread.DoInThread(from_here, std::move(task))) {
+ LOG(ERROR) << __func__ << ": failed from " << from_here.ToString();
+ return BT_STATUS_FAIL;
+ }
+ return BT_STATUS_SUCCESS;
+}
+
+static base::MessageLoop* message_loop_;
+base::MessageLoop* get_main_message_loop() { return message_loop_; }
+
+static void init_message_loop_thread() {
+ message_loop_thread.StartUp();
+ if (!message_loop_thread.IsRunning()) {
+ FAIL() << "unable to create message loop thread.";
+ }
+
+ if (!message_loop_thread.EnableRealTimeScheduling())
+ LOG(ERROR) << "Unable to set real time scheduling";
+
+ message_loop_ = message_loop_thread.message_loop();
+ if (message_loop_ == nullptr) FAIL() << "unable to get message loop.";
+}
+
+static void cleanup_message_loop_thread() {
+ message_loop_ = nullptr;
+ message_loop_thread.ShutDown();
+}
+
+using bluetooth::audio::le_audio::LeAudioClientInterface;
+
+class MockLeAudioClientInterfaceSink : public LeAudioClientInterface::Sink {
+ public:
+ MOCK_METHOD((void), Cleanup, (), (override));
+ MOCK_METHOD((void), SetPcmParameters,
+ (const LeAudioClientInterface::PcmParameters& params),
+ (override));
+ MOCK_METHOD((void), SetRemoteDelay, (uint16_t delay_report_ms), (override));
+ MOCK_METHOD((void), StartSession, (), (override));
+ MOCK_METHOD((void), StopSession, (), (override));
+ MOCK_METHOD((void), ConfirmStreamingRequest, (), (override));
+ MOCK_METHOD((void), CancelStreamingRequest, (), (override));
+ MOCK_METHOD((size_t), Read, (uint8_t * p_buf, uint32_t len));
+};
+
+class MockLeAudioClientInterfaceSource : public LeAudioClientInterface::Source {
+ public:
+ MOCK_METHOD((void), Cleanup, (), (override));
+ MOCK_METHOD((void), SetPcmParameters,
+ (const LeAudioClientInterface::PcmParameters& params),
+ (override));
+ MOCK_METHOD((void), SetRemoteDelay, (uint16_t delay_report_ms), (override));
+ MOCK_METHOD((void), StartSession, (), (override));
+ MOCK_METHOD((void), StopSession, (), (override));
+ MOCK_METHOD((void), ConfirmStreamingRequest, (), (override));
+ MOCK_METHOD((void), CancelStreamingRequest, (), (override));
+ MOCK_METHOD((size_t), Write, (const uint8_t* p_buf, uint32_t len));
+};
+
+class MockLeAudioClientInterface : public LeAudioClientInterface {
+ public:
+ MockLeAudioClientInterface() = default;
+ ~MockLeAudioClientInterface() = default;
+
+ MOCK_METHOD((Sink*), GetSink,
+ (bluetooth::audio::le_audio::StreamCallbacks stream_cb,
+ bluetooth::common::MessageLoopThread* message_loop));
+ MOCK_METHOD((Source*), GetSource,
+ (bluetooth::audio::le_audio::StreamCallbacks stream_cb,
+ bluetooth::common::MessageLoopThread* message_loop));
+};
+
+LeAudioClientInterface* mockInterface;
+
+namespace bluetooth {
+namespace audio {
+namespace le_audio {
+MockLeAudioClientInterface* interface_mock;
+MockLeAudioClientInterfaceSink* sink_mock;
+MockLeAudioClientInterfaceSource* source_mock;
+
+LeAudioClientInterface* LeAudioClientInterface::Get() { return interface_mock; }
+
+LeAudioClientInterface::Sink* LeAudioClientInterface::GetSink(
+ StreamCallbacks stream_cb,
+ bluetooth::common::MessageLoopThread* message_loop) {
+ return interface_mock->GetSink(stream_cb, message_loop);
+}
+
+LeAudioClientInterface::Source* LeAudioClientInterface::GetSource(
+ StreamCallbacks stream_cb,
+ bluetooth::common::MessageLoopThread* message_loop) {
+ return interface_mock->GetSource(stream_cb, message_loop);
+}
+
+bool LeAudioClientInterface::ReleaseSink(LeAudioClientInterface::Sink* sink) {
+ return true;
+}
+bool LeAudioClientInterface::ReleaseSource(
+ LeAudioClientInterface::Source* source) {
+ return true;
+}
+
+void LeAudioClientInterface::Sink::Cleanup() {}
+void LeAudioClientInterface::Sink::SetPcmParameters(
+ const PcmParameters& params) {}
+void LeAudioClientInterface::Sink::SetRemoteDelay(uint16_t delay_report_ms) {}
+void LeAudioClientInterface::Sink::StartSession() {}
+void LeAudioClientInterface::Sink::StopSession() {}
+void LeAudioClientInterface::Sink::ConfirmStreamingRequest(){};
+void LeAudioClientInterface::Sink::CancelStreamingRequest(){};
+
+void LeAudioClientInterface::Source::Cleanup() {}
+void LeAudioClientInterface::Source::SetPcmParameters(
+ const PcmParameters& params) {}
+void LeAudioClientInterface::Source::SetRemoteDelay(uint16_t delay_report_ms) {}
+void LeAudioClientInterface::Source::StartSession() {}
+void LeAudioClientInterface::Source::StopSession() {}
+void LeAudioClientInterface::Source::ConfirmStreamingRequest(){};
+void LeAudioClientInterface::Source::CancelStreamingRequest(){};
+
+size_t LeAudioClientInterface::Source::Write(const uint8_t* p_buf,
+ uint32_t len) {
+ return source_mock->Write(p_buf, len);
+}
+
+size_t LeAudioClientInterface::Sink::Read(uint8_t* p_buf, uint32_t len) {
+ return sink_mock->Read(p_buf, len);
+}
+} // namespace le_audio
+} // namespace audio
+} // namespace bluetooth
+
+class MockLeAudioClientAudioSinkEventReceiver
+ : public LeAudioClientAudioSinkReceiver {
+ public:
+ MOCK_METHOD((void), OnAudioDataReady, (const std::vector<uint8_t>& data),
+ (override));
+ MOCK_METHOD((void), OnAudioSuspend, (std::promise<void> do_suspend_promise),
+ (override));
+ MOCK_METHOD((void), OnAudioResume, (std::promise<void> do_resume_promise),
+ (override));
+ MOCK_METHOD((void), OnAudioMetadataUpdate,
+ (std::promise<void> do_update_metadata_promise,
+ audio_usage_t usage, audio_content_type_t content_type),
+ (override));
+};
+
+class MockLeAudioClientAudioSourceEventReceiver
+ : public LeAudioClientAudioSourceReceiver {
+ public:
+ MOCK_METHOD((void), OnAudioSuspend, (std::promise<void> do_suspend_promise),
+ (override));
+ MOCK_METHOD((void), OnAudioResume, (std::promise<void> do_resume_promise),
+ (override));
+};
+
+class LeAudioClientAudioTest : public ::testing::Test {
+ public:
+ void SetUp(void) override {
+ init_message_loop_thread();
+ bluetooth::audio::le_audio::interface_mock = &mock_client_interface_;
+ bluetooth::audio::le_audio::sink_mock = &mock_client_interface_sink_;
+ bluetooth::audio::le_audio::source_mock = &mock_client_interface_source_;
+
+ // Init sink Audio HAL mock
+ is_sink_acquired = false;
+ hal_sink_stream_cb = {.on_suspend_ = nullptr, .on_resume_ = nullptr};
+
+ ON_CALL(mock_client_interface_, GetSink(_, _))
+ .WillByDefault(DoAll(SaveArg<0>(&hal_sink_stream_cb),
+ Assign(&is_sink_acquired, true),
+ Return(bluetooth::audio::le_audio::sink_mock)));
+ ON_CALL(mock_client_interface_sink_, Cleanup())
+ .WillByDefault(Assign(&is_sink_acquired, false));
+
+ // Init source Audio HAL mock
+ is_source_acquired = false;
+ hal_source_stream_cb = {.on_suspend_ = nullptr, .on_resume_ = nullptr};
+
+ ON_CALL(mock_client_interface_, GetSource(_, _))
+ .WillByDefault(DoAll(SaveArg<0>(&hal_source_stream_cb),
+ Assign(&is_source_acquired, true),
+ Return(bluetooth::audio::le_audio::source_mock)));
+ ON_CALL(mock_client_interface_source_, Cleanup())
+ .WillByDefault(Assign(&is_source_acquired, false));
+ }
+
+ void AcquireAudioSink(void) {
+ audio_sink_instance_ = LeAudioClientAudioSink::Acquire();
+ }
+
+ void ReleaseAudioSink(void) {
+ LeAudioClientAudioSink::Release(audio_sink_instance_);
+ audio_sink_instance_ = nullptr;
+ }
+
+ void AcquireAudioSource(void) {
+ audio_source_instance_ = LeAudioClientAudioSource::Acquire();
+ }
+
+ void ReleaseAudioSource(void) {
+ LeAudioClientAudioSource::Release(audio_source_instance_);
+ audio_source_instance_ = nullptr;
+ }
+
+ void TearDown(void) override {
+ /* We have to call Cleanup to tidy up some static variables.
+ * If on the HAL end Source is running it means we are running the Sink
+ * on our end, and vice versa.
+ */
+ if (is_source_acquired == true) ReleaseAudioSink();
+ if (is_sink_acquired == true) ReleaseAudioSource();
+
+ cleanup_message_loop_thread();
+
+ bluetooth::audio::le_audio::sink_mock = nullptr;
+ bluetooth::audio::le_audio::source_mock = nullptr;
+ }
+
+ MockLeAudioClientInterface mock_client_interface_;
+ MockLeAudioClientInterfaceSink mock_client_interface_sink_;
+ MockLeAudioClientInterfaceSource mock_client_interface_source_;
+
+ MockLeAudioClientAudioSinkEventReceiver mock_hal_sink_event_receiver_;
+ MockLeAudioClientAudioSourceEventReceiver mock_hal_source_event_receiver_;
+
+ bool is_source_acquired = false;
+ bool is_sink_acquired = false;
+ const void* audio_sink_instance_ = nullptr;
+ const void* audio_source_instance_ = nullptr;
+
+ bluetooth::audio::le_audio::StreamCallbacks hal_source_stream_cb;
+ bluetooth::audio::le_audio::StreamCallbacks hal_sink_stream_cb;
+
+ const LeAudioCodecConfiguration default_codec_conf{
+ .num_channels = LeAudioCodecConfiguration::kChannelNumberMono,
+ .sample_rate = LeAudioCodecConfiguration::kSampleRate44100,
+ .bits_per_sample = LeAudioCodecConfiguration::kBitsPerSample24,
+ .data_interval_us = LeAudioCodecConfiguration::kInterval10000Us,
+ };
+};
+
+TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSinkInitializeCleanup) {
+ EXPECT_CALL(mock_client_interface_, GetSource(_, _))
+ .WillOnce(DoAll(Assign(&is_source_acquired, true),
+ Return(bluetooth::audio::le_audio::source_mock)));
+ AcquireAudioSink();
+
+ EXPECT_CALL(mock_client_interface_source_, Cleanup())
+ .WillOnce(Assign(&is_source_acquired, false));
+ ReleaseAudioSink();
+}
+
+TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSourceInitializeCleanup) {
+ EXPECT_CALL(mock_client_interface_, GetSink(_, _))
+ .WillOnce(DoAll(Assign(&is_sink_acquired, true),
+ Return(bluetooth::audio::le_audio::sink_mock)));
+ AcquireAudioSource();
+
+ EXPECT_CALL(mock_client_interface_sink_, Cleanup())
+ .WillOnce(Assign(&is_sink_acquired, false));
+ ReleaseAudioSource();
+}
+
+TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSinkStartStop) {
+ LeAudioClientInterface::PcmParameters params;
+ EXPECT_CALL(mock_client_interface_source_, SetPcmParameters(_))
+ .Times(1)
+ .WillOnce(SaveArg<0>(&params));
+ EXPECT_CALL(mock_client_interface_source_, StartSession()).Times(1);
+
+ AcquireAudioSink();
+ ASSERT_TRUE(LeAudioClientAudioSink::Start(default_codec_conf,
+ &mock_hal_source_event_receiver_));
+
+ ASSERT_EQ(params.channels_count,
+ bluetooth::audio::le_audio::kChannelNumberMono);
+ ASSERT_EQ(params.sample_rate, bluetooth::audio::le_audio::kSampleRate44100);
+ ASSERT_EQ(params.bits_per_sample,
+ bluetooth::audio::le_audio::kBitsPerSample24);
+ ASSERT_EQ(params.data_interval_us, 10000u);
+
+ EXPECT_CALL(mock_client_interface_source_, StopSession()).Times(1);
+
+ LeAudioClientAudioSink::Stop();
+}
+
+TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSourceStartStop) {
+ LeAudioClientInterface::PcmParameters params;
+ EXPECT_CALL(mock_client_interface_sink_, SetPcmParameters(_))
+ .Times(1)
+ .WillOnce(SaveArg<0>(&params));
+ EXPECT_CALL(mock_client_interface_sink_, StartSession()).Times(1);
+
+ AcquireAudioSource();
+ ASSERT_TRUE(LeAudioClientAudioSource::Start(default_codec_conf,
+ &mock_hal_sink_event_receiver_));
+
+ ASSERT_EQ(params.channels_count,
+ bluetooth::audio::le_audio::kChannelNumberMono);
+ ASSERT_EQ(params.sample_rate, bluetooth::audio::le_audio::kSampleRate44100);
+ ASSERT_EQ(params.bits_per_sample,
+ bluetooth::audio::le_audio::kBitsPerSample24);
+ ASSERT_EQ(params.data_interval_us, 10000u);
+
+ EXPECT_CALL(mock_client_interface_sink_, StopSession()).Times(1);
+
+ LeAudioClientAudioSource::Stop();
+}
+
+TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSinkSendData) {
+ AcquireAudioSink();
+ ASSERT_TRUE(LeAudioClientAudioSink::Start(default_codec_conf,
+ &mock_hal_source_event_receiver_));
+
+ const uint8_t* exp_p = nullptr;
+ uint32_t exp_len = 0;
+ uint8_t input_buf[] = {
+ 0x02,
+ 0x03,
+ 0x05,
+ 0x19,
+ };
+ ON_CALL(mock_client_interface_source_, Write(_, _))
+ .WillByDefault(DoAll(SaveArg<0>(&exp_p), SaveArg<1>(&exp_len),
+ ReturnPointee(&exp_len)));
+
+ ASSERT_EQ(LeAudioClientAudioSink::SendData(input_buf, sizeof(input_buf)),
+ sizeof(input_buf));
+ ASSERT_EQ(exp_len, sizeof(input_buf));
+ ASSERT_EQ(exp_p, input_buf);
+
+ LeAudioClientAudioSource::Stop();
+}
+
+TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSinkSuspend) {
+ AcquireAudioSink();
+ ASSERT_TRUE(LeAudioClientAudioSink::Start(default_codec_conf,
+ &mock_hal_source_event_receiver_));
+
+ ASSERT_NE(hal_source_stream_cb.on_suspend_, nullptr);
+
+ /* Expect LeAudio registered event listener to get called when HAL calls the
+ * client_audio's internal suspend callback.
+ */
+ EXPECT_CALL(mock_hal_source_event_receiver_, OnAudioSuspend(_)).Times(1);
+ ASSERT_TRUE(hal_source_stream_cb.on_suspend_());
+}
+
+TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSourceSuspend) {
+ AcquireAudioSource();
+ ASSERT_TRUE(LeAudioClientAudioSource::Start(default_codec_conf,
+ &mock_hal_sink_event_receiver_));
+
+ ASSERT_NE(hal_sink_stream_cb.on_suspend_, nullptr);
+
+ /* Expect LeAudio registered event listener to get called when HAL calls the
+ * client_audio's internal suspend callback.
+ */
+ EXPECT_CALL(mock_hal_sink_event_receiver_, OnAudioSuspend(_)).Times(1);
+ ASSERT_TRUE(hal_sink_stream_cb.on_suspend_());
+}
+
+TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSinkResume) {
+ AcquireAudioSink();
+ ASSERT_TRUE(LeAudioClientAudioSink::Start(default_codec_conf,
+ &mock_hal_source_event_receiver_));
+
+ ASSERT_NE(hal_source_stream_cb.on_resume_, nullptr);
+
+ /* Expect LeAudio registered event listener to get called when HAL calls the
+ * client_audio's internal resume callback.
+ */
+ EXPECT_CALL(mock_hal_source_event_receiver_, OnAudioResume(_)).Times(1);
+ bool start_media_task = false;
+ ASSERT_TRUE(hal_source_stream_cb.on_resume_(start_media_task));
+}
+
+TEST_F(LeAudioClientAudioTest,
+ testLeAudioClientAudioSourceResumeStartSourceTask) {
+ const LeAudioCodecConfiguration codec_conf{
+ .num_channels = LeAudioCodecConfiguration::kChannelNumberStereo,
+ .sample_rate = LeAudioCodecConfiguration::kSampleRate16000,
+ .bits_per_sample = LeAudioCodecConfiguration::kBitsPerSample24,
+ .data_interval_us = LeAudioCodecConfiguration::kInterval10000Us,
+ };
+ AcquireAudioSource();
+ ASSERT_TRUE(LeAudioClientAudioSource::Start(codec_conf,
+ &mock_hal_sink_event_receiver_));
+
+ std::chrono::time_point<std::chrono::system_clock> resumed_ts;
+ std::chrono::time_point<std::chrono::system_clock> executed_ts;
+ std::promise<void> promise;
+ auto future = promise.get_future();
+
+ uint32_t calculated_bytes_per_tick = 0;
+ EXPECT_CALL(mock_client_interface_sink_, Read(_, _))
+ .Times(AtLeast(1))
+ .WillOnce(Invoke([&](uint8_t* p_buf, uint32_t len) -> uint32_t {
+ executed_ts = std::chrono::system_clock::now();
+ calculated_bytes_per_tick = len;
+
+ // fake some data from audio framework
+ for (uint32_t i = 0u; i < len; ++i) {
+ p_buf[i] = i;
+ }
+
+ // Return exactly as much data as requested
+ promise.set_value();
+ return len;
+ }));
+
+ std::promise<void> data_promise;
+ auto data_future = data_promise.get_future();
+
+ /* Expect this callback to be called to Client by the HAL glue layer */
+ std::vector<uint8_t> media_data_to_send;
+ EXPECT_CALL(mock_hal_sink_event_receiver_, OnAudioDataReady(_))
+ .WillOnce(Invoke([&](const std::vector<uint8_t>& data) -> void {
+ media_data_to_send = std::move(data);
+ data_promise.set_value();
+ }));
+
+ /* Expect LeAudio registered event listener to get called when HAL calls the
+ * client_audio's internal resume callback.
+ */
+ ASSERT_NE(hal_sink_stream_cb.on_resume_, nullptr);
+ EXPECT_CALL(mock_hal_sink_event_receiver_, OnAudioResume(_)).Times(1);
+ resumed_ts = std::chrono::system_clock::now();
+ bool start_media_task = true;
+ ASSERT_TRUE(hal_sink_stream_cb.on_resume_(start_media_task));
+ LeAudioClientAudioSource::ConfirmStreamingRequest();
+
+ ASSERT_EQ(future.wait_for(std::chrono::seconds(1)),
+ std::future_status::ready);
+
+ ASSERT_EQ(data_future.wait_for(std::chrono::seconds(1)),
+ std::future_status::ready);
+
+ // Check agains expected payload size
+ const uint32_t channel_bytes_per_sample = (24 /*bps*/ / 8);
+ const uint32_t channel_bytes_per_10ms_at_16000Hz =
+ ((10ms).count() * channel_bytes_per_sample * 16000 /*Hz*/) /
+ (1000ms).count();
+
+ // Expect 2 channel (stereo) data
+ ASSERT_EQ(calculated_bytes_per_tick, 2 * channel_bytes_per_10ms_at_16000Hz);
+
+ // Verify callback call interval for the requested 10ms (+2ms error margin)
+ auto delta = std::chrono::duration_cast<std::chrono::milliseconds>(
+ executed_ts - resumed_ts);
+ EXPECT_TRUE((delta >= 10ms) && (delta <= 12ms));
+
+ // Verify if we got just right amount of data in the callback call
+ ASSERT_EQ(media_data_to_send.size(), calculated_bytes_per_tick);
+}
+
+TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSourceResume) {
+ AcquireAudioSource();
+ ASSERT_TRUE(LeAudioClientAudioSource::Start(default_codec_conf,
+ &mock_hal_sink_event_receiver_));
+
+ ASSERT_NE(hal_sink_stream_cb.on_resume_, nullptr);
+
+ /* Expect LeAudio registered event listener to get called when HAL calls the
+ * client_audio's internal resume callback.
+ */
+ EXPECT_CALL(mock_hal_sink_event_receiver_, OnAudioResume(_)).Times(1);
+ bool start_media_task = false;
+ ASSERT_TRUE(hal_sink_stream_cb.on_resume_(start_media_task));
+}
diff --git a/bta/le_audio/client_parser.cc b/bta/le_audio/client_parser.cc
new file mode 100644
index 000000000..cb6936d0f
--- /dev/null
+++ b/bta/le_audio/client_parser.cc
@@ -0,0 +1,628 @@
+/*
+ * Copyright 2019 HIMSA II K/S - www.himsa.com. Represented by EHIMA -
+ * www.ehima.com
+ *
+ * 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 module contains API of the audio stream control protocol.
+ */
+
+#include "client_parser.h"
+
+#include <base/strings/string_number_conversions.h>
+#include <endian.h>
+#include <hardware/bt_common_types.h>
+
+#include <map>
+#include <memory>
+#include <numeric>
+
+#include "bta_le_audio_api.h"
+#include "gap_api.h"
+#include "le_audio_types.h"
+#include "osi/include/allocator.h"
+
+using le_audio::types::acs_ac_record;
+
+namespace le_audio {
+namespace client_parser {
+namespace ascs {
+static std::map<uint8_t, std::string> ase_state_map_string = {
+ {kAseStateIdle, "Idle"},
+ {kAseStateCodecConfigured, "Codec Configured"},
+ {kAseStateQosConfigured, "QoS Configured"},
+ {kAseStateEnabling, "Enabling"},
+ {kAseStateStreaming, "Streaming"},
+ {kAseStateDisabling, "Disabling"},
+ {kAseStateReleasing, "Releasing"},
+};
+
+static std::map<uint8_t, std::string> ctp_opcode_map_string = {
+ {kCtpOpcodeCodecConfiguration, "Config Codec"},
+ {kCtpOpcodeQosConfiguration, "Config QoS"},
+ {kCtpOpcodeEnable, "Enable"},
+ {kCtpOpcodeReceiverStartReady, "Receiver Start Ready"},
+ {kCtpOpcodeDisable, "Disable"},
+ {kCtpOpcodeReceiverStopReady, "Receiver Stop Ready"},
+ {kCtpOpcodeUpdateMetadata, "Update Metadata"},
+ {kCtpOpcodeRelease, "Release"},
+};
+
+static std::map<uint8_t, std::string> ctp_reason_map_string = {
+ {kCtpResponseNoReason, ""},
+ {kCtpResponseCodecId, "Codec ID"},
+ {kCtpResponseCodecSpecificConfiguration, "Codec specific configuration"},
+ {kCtpResponseSduInterval, "SDU interval"},
+ {kCtpResponseFraming, "Framing"},
+ {kCtpResponsePhy, "PHY"},
+ {kCtpResponseMaximumSduSize, "Maximum SDU size"},
+ {kCtpResponseRetransmissionNumber, "Retransmission number"},
+ {kCtpResponseMaxTransportLatency, "Max Transport latency"},
+ {kCtpResponsePresentationDelay, "Presentation delay"},
+ {kCtpResponseInvalidAseCisMapping, "Invalid ASE CIS mapping"},
+};
+
+static std::map<uint8_t, std::string> ctp_response_code_map_string = {
+ {kCtpResponseCodeSuccess, "Success"},
+ {kCtpResponseCodeUnsupportedOpcode, "Unsupported Opcode"},
+ {kCtpResponseCodeInvalidLength, "Invalid Length"},
+ {kCtpResponseCodeInvalidAseId, "Invalid ASE ID"},
+ {kCtpResponseCodeInvalidAseStateMachineTransition,
+ "Invalid ASE State Machine Transition"},
+ {kCtpResponseCodeInvalidAseDirection, "Invalid ASE Direction"},
+ {kCtpResponseCodeUnsupportedAudioCapabilities,
+ "Unsupported Audio Capabilities"},
+ {kCtpResponseCodeUnsupportedConfigurationParameterValue,
+ "Unsupported Configuration Parameter Value"},
+ {kCtpResponseCodeRejectedConfigurationParameterValue,
+ "Rejected Configuration Parameter Value"},
+ {kCtpResponseCodeInvalidConfigurationParameterValue,
+ "Invalid Configuration Parameter Value"},
+ {kCtpResponseCodeUnsupportedMetadata, "Unsupported Metadata"},
+ {kCtpResponseCodeRejectedMetadata, "Rejected Metadata"},
+ {kCtpResponseCodeInvalidMetadata, "Invalid Metadata"},
+ {kCtpResponseCodeInsufficientResources, "Insufficient Resources"},
+ {kCtpResponseCodeUnspecifiedError, "Unspecified Error"},
+};
+
+bool ParseAseStatusHeader(ase_rsp_hdr& arh, uint16_t len,
+ const uint8_t* value) {
+ if (len < kAseRspHdrMinLen) {
+ LOG(ERROR) << __func__
+ << ", wrong len of ASE char (header): " << static_cast<int>(len);
+
+ return false;
+ }
+
+ STREAM_TO_UINT8(arh.id, value);
+ STREAM_TO_UINT8(arh.state, value);
+
+ LOG(INFO) << "ASE status: "
+ << "\tASE id: " << loghex(arh.id)
+ << "\tASE state: " << ase_state_map_string[arh.state] << " ("
+ << loghex(arh.state) << ")";
+
+ return true;
+}
+
+bool ParseAseStatusCodecConfiguredStateParams(
+ struct ase_codec_configured_state_params& rsp, uint16_t len,
+ const uint8_t* value) {
+ uint8_t codec_spec_conf_len;
+
+ if (len < kAseStatusCodecConfMinLen) {
+ LOG(ERROR) << "Wrong len of codec conf status (Codec conf header)";
+ return false;
+ }
+
+ STREAM_TO_UINT8(rsp.framing, value);
+ STREAM_TO_UINT8(rsp.preferred_phy, value);
+ STREAM_TO_UINT8(rsp.preferred_retrans_nb, value);
+ STREAM_TO_UINT16(rsp.max_transport_latency, value);
+ STREAM_TO_UINT24(rsp.pres_delay_min, value);
+ STREAM_TO_UINT24(rsp.pres_delay_max, value);
+ STREAM_TO_UINT24(rsp.preferred_pres_delay_min, value);
+ STREAM_TO_UINT24(rsp.preferred_pres_delay_max, value);
+ STREAM_TO_UINT8(rsp.codec_id.coding_format, value);
+ STREAM_TO_UINT16(rsp.codec_id.vendor_company_id, value);
+ STREAM_TO_UINT16(rsp.codec_id.vendor_codec_id, value);
+ STREAM_TO_UINT8(codec_spec_conf_len, value);
+
+ len -= kAseStatusCodecConfMinLen;
+
+ if (len != codec_spec_conf_len) {
+ LOG(ERROR) << "Wrong len of codec conf status (Codec spec conf)";
+ return false;
+ }
+ if (codec_spec_conf_len)
+ rsp.codec_spec_conf =
+ std::vector<uint8_t>(value, value + codec_spec_conf_len);
+
+ LOG(INFO) << __func__ << ", Codec configuration"
+ << "\n\tFraming: " << loghex(rsp.framing)
+ << "\n\tPreferred PHY: " << loghex(rsp.preferred_phy)
+ << "\n\tPreferred retransmission number: "
+ << loghex(rsp.preferred_retrans_nb) << "\n\tMax transport latency: "
+ << loghex(rsp.max_transport_latency)
+ << "\n\tPresence delay min: " << loghex(rsp.pres_delay_min)
+ << "\n\tPresence delay max: " << loghex(rsp.pres_delay_max)
+ << "\n\tPreferredPresentationDelayMin: "
+ << loghex(rsp.preferred_pres_delay_min)
+ << "\n\tPreferredPresentationDelayMax: "
+ << loghex(rsp.preferred_pres_delay_max)
+ << "\n\tCoding format: " << loghex(rsp.codec_id.coding_format)
+ << "\n\tVendor codec company ID: "
+ << loghex(rsp.codec_id.vendor_company_id)
+ << "\n\tVendor codec ID: " << loghex(rsp.codec_id.vendor_codec_id)
+ << "\n\tCodec specific conf len: " << (int)codec_spec_conf_len
+ << "\n\tCodec specific conf: "
+ << base::HexEncode(rsp.codec_spec_conf.data(),
+ rsp.codec_spec_conf.size());
+
+ return true;
+}
+
+bool ParseAseStatusQosConfiguredStateParams(
+ struct ase_qos_configured_state_params& rsp, uint16_t len,
+ const uint8_t* value) {
+ if (len != kAseStatusCodecQosConfMinLen) {
+ LOG(ERROR) << "Wrong len of ASE characteristic (QOS conf header)";
+ return false;
+ }
+
+ STREAM_TO_UINT8(rsp.cig_id, value);
+ STREAM_TO_UINT8(rsp.cis_id, value);
+ STREAM_TO_UINT24(rsp.sdu_interval, value);
+ STREAM_TO_UINT8(rsp.framing, value);
+ STREAM_TO_UINT8(rsp.phy, value);
+ STREAM_TO_UINT16(rsp.max_sdu, value);
+ STREAM_TO_UINT8(rsp.retrans_nb, value);
+ STREAM_TO_UINT16(rsp.max_transport_latency, value);
+ STREAM_TO_UINT24(rsp.pres_delay, value);
+
+ LOG(INFO) << __func__ << ", Codec QoS Configured"
+ << "\n\tCIG: " << loghex(rsp.cig_id)
+ << "\n\tCIS: " << loghex(rsp.cis_id)
+ << "\n\tSDU interval: " << loghex(rsp.sdu_interval)
+ << "\n\tFraming: " << loghex(rsp.framing)
+ << "\n\tPHY: " << loghex(rsp.phy)
+ << "\n\tMax SDU: " << loghex(rsp.max_sdu)
+ << "\n\tRetransmission number: " << loghex(rsp.retrans_nb)
+ << "\n\tMax transport latency: "
+ << loghex(rsp.max_transport_latency)
+ << "\n\tPresentation delay: " << loghex(rsp.pres_delay);
+
+ return true;
+}
+
+bool ParseAseStatusTransientStateParams(struct ase_transient_state_params& rsp,
+ uint16_t len, const uint8_t* value) {
+ uint8_t metadata_len;
+
+ if (len < kAseStatusTransMinLen) {
+ LOG(ERROR) << "Wrong len of ASE characteristic (metadata)";
+ return false;
+ }
+
+ STREAM_TO_UINT8(metadata_len, value);
+ len -= kAseStatusTransMinLen;
+
+ if (len != metadata_len) {
+ LOG(ERROR) << "Wrong len of ASE characteristic (metadata)";
+ return false;
+ }
+
+ if (metadata_len > 0)
+ rsp.metadata = std::vector<uint8_t>(value, value + metadata_len);
+
+ LOG(INFO) << __func__ << ", Status enabling/streaming/disabling metadata:"
+ << base::HexEncode(rsp.metadata.data(), rsp.metadata.size());
+
+ return true;
+}
+
+bool ParseAseCtpNotification(struct ctp_ntf& ntf, uint16_t len,
+ const uint8_t* value) {
+ uint8_t num_entries;
+
+ if (len < kCtpNtfMinLen) {
+ LOG(ERROR) << "Wrong len of ASE control point notification: " << (int)len;
+ return false;
+ }
+
+ STREAM_TO_UINT8(ntf.op, value);
+ STREAM_TO_UINT8(num_entries, value);
+
+ if (len != kCtpNtfMinLen + (num_entries * kCtpAseEntryMinLen)) {
+ LOG(ERROR) << "Wrong len of ASE control point notification (ASE IDs)";
+ return false;
+ }
+
+ for (int i = 0; i < num_entries; i++) {
+ struct ctp_ase_entry entry;
+
+ STREAM_TO_UINT8(entry.ase_id, value);
+ STREAM_TO_UINT8(entry.response_code, value);
+ STREAM_TO_UINT8(entry.reason, value);
+
+ ntf.entries.push_back(std::move(entry));
+ }
+
+ LOG(INFO) << __func__ << ", Control point notification"
+ << "\n\tOpcode: " << ctp_opcode_map_string[ntf.op] << " ("
+ << loghex(ntf.op) << ")"
+ << "\n\tNum ASE IDs: " << (int)num_entries;
+ for (size_t i = 0; i < num_entries; i++)
+ LOG(INFO) << "\n\tASE ID[" << loghex(ntf.entries[i].ase_id)
+ << "] response: "
+ << ctp_response_code_map_string[ntf.entries[i].response_code]
+ << " (" << loghex(ntf.entries[i].response_code) << ")"
+ << " reason: " << ctp_reason_map_string[ntf.entries[i].reason]
+ << " (" << loghex(ntf.entries[i].reason) << ")";
+
+ return true;
+}
+
+bool PrepareAseCtpCodecConfig(const std::vector<struct ctp_codec_conf>& confs,
+ std::vector<uint8_t>& value) {
+ if (confs.size() == 0) return false;
+
+ std::string conf_ents_str;
+ size_t msg_len = std::accumulate(
+ confs.begin(), confs.end(),
+ confs.size() * kCtpCodecConfMinLen + kAseNumSize + kCtpOpSize,
+ [&conf_ents_str](size_t cur_len, auto const& conf) {
+ auto ltv_map = conf.codec_config.GetAsLtvMap();
+ for (const auto& [type, value] : ltv_map.Values()) {
+ conf_ents_str +=
+ "\ttype: " + std::to_string(type) +
+ "\tlen: " + std::to_string(value.size()) +
+ "\tdata: " + base::HexEncode(value.data(), value.size()) + "\n";
+ };
+
+ return cur_len + ltv_map.RawPacketSize();
+ });
+ value.resize(msg_len);
+
+ uint8_t* msg = value.data();
+ UINT8_TO_STREAM(msg, kCtpOpcodeCodecConfiguration);
+
+ UINT8_TO_STREAM(msg, confs.size());
+ for (const struct ctp_codec_conf& conf : confs) {
+ UINT8_TO_STREAM(msg, conf.ase_id);
+ UINT8_TO_STREAM(msg, conf.target_latency);
+ UINT8_TO_STREAM(msg, conf.target_phy);
+ UINT8_TO_STREAM(msg, conf.codec_id.coding_format);
+ UINT16_TO_STREAM(msg, conf.codec_id.vendor_company_id);
+ UINT16_TO_STREAM(msg, conf.codec_id.vendor_codec_id);
+
+ auto ltv_map = conf.codec_config.GetAsLtvMap();
+ auto codec_spec_conf_len = ltv_map.RawPacketSize();
+
+ UINT8_TO_STREAM(msg, codec_spec_conf_len);
+ msg = ltv_map.RawPacket(msg);
+
+ LOG(INFO) << __func__ << ", Codec configuration"
+ << "\n\tAse id: " << loghex(conf.ase_id)
+ << "\n\tTarget latency: " << loghex(conf.target_latency)
+ << "\n\tTarget PHY: " << loghex(conf.target_phy)
+ << "\n\tCoding format: " << loghex(conf.codec_id.coding_format)
+ << "\n\tVendor codec company ID: "
+ << loghex(conf.codec_id.vendor_company_id)
+ << "\n\tVendor codec ID: "
+ << loghex(conf.codec_id.vendor_codec_id)
+ << "\n\tCodec config len: "
+ << static_cast<int>(codec_spec_conf_len)
+ << "\n\tCodec spec conf: "
+ << "\n"
+ << conf_ents_str;
+ }
+
+ return true;
+}
+
+bool PrepareAseCtpConfigQos(const std::vector<struct ctp_qos_conf>& confs,
+ std::vector<uint8_t>& value) {
+ if (confs.size() == 0) return false;
+ value.resize(confs.size() * kCtpQosConfMinLen + kAseNumSize + kCtpOpSize);
+
+ uint8_t* msg = value.data();
+ UINT8_TO_STREAM(msg, kCtpOpcodeQosConfiguration);
+ UINT8_TO_STREAM(msg, confs.size());
+
+ for (const struct ctp_qos_conf& conf : confs) {
+ UINT8_TO_STREAM(msg, conf.ase_id);
+ UINT8_TO_STREAM(msg, conf.cig);
+ UINT8_TO_STREAM(msg, conf.cis);
+ UINT24_TO_STREAM(msg, conf.sdu_interval);
+ UINT8_TO_STREAM(msg, conf.framing);
+ UINT8_TO_STREAM(msg, conf.phy);
+ UINT16_TO_STREAM(msg, conf.max_sdu);
+ UINT8_TO_STREAM(msg, conf.retrans_nb);
+ UINT16_TO_STREAM(msg, conf.max_transport_latency);
+ UINT24_TO_STREAM(msg, conf.pres_delay);
+
+ LOG(INFO) << __func__ << ", QoS configuration"
+ << "\n\tAse id: " << loghex(conf.ase_id)
+ << "\n\tcig: " << loghex(conf.cig)
+ << "\n\tCis: " << loghex(conf.cis)
+ << "\n\tSDU interval: " << loghex(conf.sdu_interval)
+ << "\n\tFraming: " << loghex(conf.framing)
+ << "\n\tPhy: " << loghex(conf.phy)
+ << "\n\tMax sdu size: " << loghex(conf.max_sdu)
+ << "\n\tRetrans nb: " << loghex(conf.retrans_nb)
+ << "\n\tMax Transport latency: "
+ << loghex(conf.max_transport_latency)
+ << "\n\tPres delay: " << loghex(conf.pres_delay);
+ }
+
+ return true;
+}
+
+bool PrepareAseCtpEnable(const std::vector<struct ctp_enable>& confs,
+ std::vector<uint8_t>& value) {
+ if (confs.size() == 0) return false;
+
+ uint16_t msg_len = confs.size() * kCtpEnableMinLen + kAseNumSize + kCtpOpSize;
+ std::for_each(confs.begin(), confs.end(),
+ [&msg_len](const struct ctp_enable& conf) {
+ msg_len += conf.metadata.size();
+ });
+ value.resize(msg_len);
+
+ uint8_t* msg = value.data();
+ UINT8_TO_STREAM(msg, kCtpOpcodeEnable);
+ UINT8_TO_STREAM(msg, confs.size());
+
+ for (const struct ctp_enable& conf : confs) {
+ UINT8_TO_STREAM(msg, conf.ase_id);
+ UINT8_TO_STREAM(msg, conf.metadata.size());
+ ARRAY_TO_STREAM(msg, conf.metadata.data(),
+ static_cast<int>(conf.metadata.size()));
+
+ LOG(INFO) << __func__ << ", Enable"
+ << "\n\tAse id: " << loghex(conf.ase_id) << "\n\tMetadata: "
+ << base::HexEncode(conf.metadata.data(), conf.metadata.size());
+ }
+
+ return true;
+}
+
+bool PrepareAseCtpAudioReceiverStartReady(const std::vector<uint8_t>& ase_ids,
+ std::vector<uint8_t>& value) {
+ if (ase_ids.size() == 0) return false;
+ value.resize(ase_ids.size() * kAseIdSize + kAseNumSize + kCtpOpSize);
+
+ uint8_t* msg = value.data();
+ UINT8_TO_STREAM(msg, kCtpOpcodeReceiverStartReady);
+ UINT8_TO_STREAM(msg, ase_ids.size());
+
+ for (const uint8_t& id : ase_ids) {
+ UINT8_TO_STREAM(msg, id);
+
+ LOG(INFO) << __func__ << ", ReceiverStartReady"
+ << "\n\tAse id: " << loghex(id);
+ }
+
+ return true;
+}
+
+bool PrepareAseCtpDisable(const std::vector<uint8_t>& ase_ids,
+ std::vector<uint8_t>& value) {
+ if (ase_ids.size() == 0) return false;
+ value.resize(ase_ids.size() * kAseIdSize + kAseNumSize + kCtpOpSize);
+
+ uint8_t* msg = value.data();
+ UINT8_TO_STREAM(msg, kCtpOpcodeDisable);
+ UINT8_TO_STREAM(msg, ase_ids.size());
+
+ for (const uint8_t& id : ase_ids) {
+ UINT8_TO_STREAM(msg, id);
+
+ LOG(INFO) << __func__ << ", Disable"
+ << "\n\tAse id: " << loghex(id);
+ }
+
+ return true;
+}
+
+bool PrepareAseCtpAudioReceiverStopReady(const std::vector<uint8_t>& ase_ids,
+ std::vector<uint8_t>& value) {
+ if (ase_ids.size() == 0) return false;
+ value.resize(ase_ids.size() * kAseIdSize + kAseNumSize + kCtpOpSize);
+
+ uint8_t* msg = value.data();
+ UINT8_TO_STREAM(msg, kCtpOpcodeReceiverStopReady);
+ UINT8_TO_STREAM(msg, ase_ids.size());
+
+ for (const uint8_t& ase_id : ase_ids) {
+ UINT8_TO_STREAM(msg, ase_id);
+
+ LOG(INFO) << __func__ << ", ReceiverStopReady"
+ << "\n\tAse id: " << loghex(ase_id);
+ }
+
+ return true;
+}
+
+bool PrepareAseCtpUpdateMetadata(
+ const std::vector<struct ctp_update_metadata>& confs,
+ std::vector<uint8_t>& value) {
+ if (confs.size() == 0) return false;
+
+ uint16_t msg_len =
+ confs.size() * kCtpUpdateMetadataMinLen + kAseNumSize + kCtpOpSize;
+ std::for_each(confs.begin(), confs.end(),
+ [&msg_len](const struct ctp_update_metadata& conf) {
+ msg_len += conf.metadata.size();
+ });
+ value.resize(msg_len);
+
+ uint8_t* msg = value.data();
+ UINT8_TO_STREAM(msg, kCtpOpcodeUpdateMetadata);
+ UINT8_TO_STREAM(msg, confs.size());
+
+ for (const struct ctp_update_metadata& conf : confs) {
+ UINT8_TO_STREAM(msg, conf.ase_id);
+ UINT8_TO_STREAM(msg, conf.metadata.size());
+ ARRAY_TO_STREAM(msg, conf.metadata.data(),
+ static_cast<int>(conf.metadata.size()));
+
+ LOG(INFO) << __func__ << ", Update Metadata"
+ << "\n\tAse id: " << loghex(conf.ase_id) << "\n\tMetadata: "
+ << base::HexEncode(conf.metadata.data(), conf.metadata.size());
+ }
+
+ return true;
+}
+
+bool PrepareAseCtpRelease(const std::vector<uint8_t>& ase_ids,
+ std::vector<uint8_t>& value) {
+ if (ase_ids.size() == 0) return true;
+ value.resize(ase_ids.size() * kAseIdSize + kAseNumSize + kCtpOpSize);
+
+ uint8_t* msg = value.data();
+ UINT8_TO_STREAM(msg, kCtpOpcodeRelease);
+ UINT8_TO_STREAM(msg, ase_ids.size());
+
+ for (const uint8_t& ase_id : ase_ids) {
+ UINT8_TO_STREAM(msg, ase_id);
+
+ LOG(INFO) << __func__ << ", Release"
+ << "\n\tAse id: " << loghex(ase_id);
+ }
+
+ return true;
+}
+} // namespace ascs
+
+namespace pacs {
+
+bool ParsePac(std::vector<struct acs_ac_record>& pac_recs, uint16_t len,
+ const uint8_t* value) {
+ if (len < kAcsPacDiscoverRspMinLen) {
+ LOG(ERROR) << "Wrong len of PAC characteristic";
+ return false;
+ }
+
+ uint8_t pac_rec_nb;
+ STREAM_TO_UINT8(pac_rec_nb, value);
+ len -= kAcsPacDiscoverRspMinLen;
+
+ pac_recs.reserve(pac_rec_nb);
+ for (int i = 0; i < pac_rec_nb; i++) {
+ struct acs_ac_record rec;
+ uint8_t codec_spec_cap_len, metadata_len;
+
+ if (len < kAcsPacRecordMinLen) {
+ LOG(ERROR) << "Wrong len of PAC record";
+ pac_recs.clear();
+ return false;
+ }
+
+ STREAM_TO_UINT8(rec.codec_id.coding_format, value);
+ STREAM_TO_UINT16(rec.codec_id.vendor_company_id, value);
+ STREAM_TO_UINT16(rec.codec_id.vendor_codec_id, value);
+ STREAM_TO_UINT8(codec_spec_cap_len, value);
+ len -= kAcsPacRecordMinLen - kAcsPacMetadataLenLen;
+
+ if (len < codec_spec_cap_len + kAcsPacMetadataLenLen) {
+ LOG(ERROR) << "Wrong len of PAC record (codec specific capabilities)";
+ pac_recs.clear();
+ return false;
+ }
+
+ bool parsed;
+ rec.codec_spec_caps =
+ types::LeAudioLtvMap::Parse(value, codec_spec_cap_len, parsed);
+ if (!parsed) return false;
+
+ value += codec_spec_cap_len;
+ len -= codec_spec_cap_len;
+
+ STREAM_TO_UINT8(metadata_len, value);
+ len -= kAcsPacMetadataLenLen;
+
+ if (len < metadata_len) {
+ LOG(ERROR) << "Wrong len of PAC record (metadata)";
+ pac_recs.clear();
+ return false;
+ }
+
+ rec.metadata = std::vector<uint8_t>(value, value + metadata_len);
+ value += metadata_len;
+ len -= metadata_len;
+
+ pac_recs.push_back(std::move(rec));
+ }
+
+ return true;
+}
+
+bool ParseAudioLocations(types::AudioLocations& audio_locations, uint16_t len,
+ const uint8_t* value) {
+ if (len != kAudioLocationsRspMinLen) {
+ LOG(ERROR) << "Wrong len of Audio Location characteristic";
+ return false;
+ }
+
+ STREAM_TO_UINT32(audio_locations, value);
+
+ LOG(INFO) << "Audio locations: " << audio_locations.to_string();
+
+ return true;
+}
+
+bool ParseSupportedAudioContexts(struct acs_supported_audio_contexts& contexts,
+ uint16_t len, const uint8_t* value) {
+ if (len != kAseAudioSuppContRspMinLen) {
+ LOG(ERROR) << "Wrong len of Audio Supported Context characteristic";
+ return false;
+ }
+
+ STREAM_TO_UINT16(contexts.snk_supp_cont, value);
+ STREAM_TO_UINT16(contexts.src_supp_cont, value);
+
+ LOG(INFO) << "Supported Audio Contexts: "
+ << "\n\tSupported Sink Contexts: "
+ << contexts.snk_supp_cont.to_string()
+ << "\n\tSupported Source Contexts: "
+ << contexts.src_supp_cont.to_string();
+
+ return true;
+}
+
+bool ParseAvailableAudioContexts(struct acs_available_audio_contexts& contexts,
+ uint16_t len, const uint8_t* value) {
+ if (len != kAseAudioAvailRspMinLen) {
+ LOG(ERROR) << "Wrong len of Audio Availability characteristic";
+ return false;
+ }
+
+ STREAM_TO_UINT16(contexts.snk_avail_cont, value);
+ STREAM_TO_UINT16(contexts.src_avail_cont, value);
+
+ LOG(INFO) << "Available Audio Contexts: "
+ << "\n\tAvailable Sink Contexts: "
+ << contexts.snk_avail_cont.to_string()
+ << "\n\tAvailable Source Contexts: "
+ << contexts.src_avail_cont.to_string();
+
+ return true;
+}
+} // namespace pacs
+
+} // namespace client_parser
+} // namespace le_audio
diff --git a/bta/le_audio/client_parser.h b/bta/le_audio/client_parser.h
new file mode 100644
index 000000000..f18d79d2f
--- /dev/null
+++ b/bta/le_audio/client_parser.h
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2019 HIMSA II K/S - www.himsa.com. Represented by EHIMA -
+ * www.ehima.com
+ *
+ * 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 contains the Audio Stream Control (LE_AUDIO) API function external
+ * definitions.
+ */
+
+#pragma once
+
+#include "le_audio_types.h"
+
+namespace le_audio {
+namespace client_parser {
+namespace ascs {
+/*
+ * All structures and defines are described in Audio Stream Control Service
+ * specification
+ */
+
+constexpr uint8_t kCtpResponseCodeSuccess = 0x00;
+constexpr uint8_t kCtpResponseCodeUnsupportedOpcode = 0x01;
+constexpr uint8_t kCtpResponseCodeInvalidLength = 0x02;
+constexpr uint8_t kCtpResponseCodeInvalidAseId = 0x03;
+constexpr uint8_t kCtpResponseCodeInvalidAseStateMachineTransition = 0x04;
+constexpr uint8_t kCtpResponseCodeInvalidAseDirection = 0x05;
+constexpr uint8_t kCtpResponseCodeUnsupportedAudioCapabilities = 0x06;
+constexpr uint8_t kCtpResponseCodeUnsupportedConfigurationParameterValue = 0x07;
+constexpr uint8_t kCtpResponseCodeRejectedConfigurationParameterValue = 0x08;
+constexpr uint8_t kCtpResponseCodeInvalidConfigurationParameterValue = 0x09;
+constexpr uint8_t kCtpResponseCodeUnsupportedMetadata = 0x0A;
+constexpr uint8_t kCtpResponseCodeRejectedMetadata = 0x0B;
+constexpr uint8_t kCtpResponseCodeInvalidMetadata = 0x0C;
+constexpr uint8_t kCtpResponseCodeInsufficientResources = 0x0D;
+constexpr uint8_t kCtpResponseCodeUnspecifiedError = 0x0E;
+
+constexpr uint8_t kCtpResponseNoReason = 0x00;
+constexpr uint8_t kCtpResponseCodecId = 0x01;
+constexpr uint8_t kCtpResponseCodecSpecificConfiguration = 0x02;
+constexpr uint8_t kCtpResponseSduInterval = 0x03;
+constexpr uint8_t kCtpResponseFraming = 0x04;
+constexpr uint8_t kCtpResponsePhy = 0x05;
+constexpr uint8_t kCtpResponseMaximumSduSize = 0x06;
+constexpr uint8_t kCtpResponseRetransmissionNumber = 0x07;
+constexpr uint8_t kCtpResponseMaxTransportLatency = 0x08;
+constexpr uint8_t kCtpResponsePresentationDelay = 0x09;
+constexpr uint8_t kCtpResponseInvalidAseCisMapping = 0x0A;
+
+constexpr uint8_t kLeAudioErrorCtpUnsupporterdOpcode = 0xFF;
+constexpr uint8_t kLeAudioErrorCtpTruncatedOperation = 0xFE;
+constexpr uint8_t kLeAudioErrorCtpCtpErr = 0xFD;
+
+/* ASE states */
+constexpr uint8_t kAseStateIdle = 0x00;
+constexpr uint8_t kAseStateCodecConfigured = 0x01;
+constexpr uint8_t kAseStateQosConfigured = 0x02;
+constexpr uint8_t kAseStateEnabling = 0x03;
+constexpr uint8_t kAseStateStreaming = 0x04;
+constexpr uint8_t kAseStateDisabling = 0x05;
+constexpr uint8_t kAseStateReleasing = 0x06;
+
+/* Control point opcodes */
+constexpr uint8_t kCtpOpcodeCodecConfiguration = 0x01;
+constexpr uint8_t kCtpOpcodeQosConfiguration = 0x02;
+constexpr uint8_t kCtpOpcodeEnable = 0x03;
+constexpr uint8_t kCtpOpcodeReceiverStartReady = 0x04;
+constexpr uint8_t kCtpOpcodeDisable = 0x05;
+constexpr uint8_t kCtpOpcodeReceiverStopReady = 0x06;
+constexpr uint8_t kCtpOpcodeUpdateMetadata = 0x07;
+constexpr uint8_t kCtpOpcodeRelease = 0x08;
+
+/* ASE status masks */
+static constexpr uint32_t kAseRspHeaderMaskCtrlStatusFailureOpcode = 0x00FF0000;
+static constexpr uint32_t kAseRspHeaderMaskCtrlStatusErrorCode = 0x0000FF00;
+static constexpr uint32_t kAseRspHeaderMaskCtrlStatusErrorReason = 0x000000FF;
+
+constexpr uint16_t kAseStatusCodecConfMinLen = 23;
+struct ase_codec_configured_state_params {
+ uint8_t framing;
+ uint8_t preferred_phy;
+ uint8_t preferred_retrans_nb;
+ uint16_t max_transport_latency;
+ uint32_t pres_delay_min;
+ uint32_t pres_delay_max;
+ uint32_t preferred_pres_delay_min;
+ uint32_t preferred_pres_delay_max;
+ types::LeAudioCodecId codec_id;
+ std::vector<uint8_t> codec_spec_conf;
+};
+
+constexpr uint16_t kAseStatusCodecQosConfMinLen = 15;
+struct ase_qos_configured_state_params {
+ uint8_t cig_id;
+ uint8_t cis_id;
+ uint32_t sdu_interval;
+ uint8_t framing;
+ uint8_t phy;
+ uint16_t max_sdu;
+ uint8_t retrans_nb;
+ uint16_t max_transport_latency;
+ uint32_t pres_delay;
+};
+
+constexpr uint16_t kAseStatusTransMinLen = 1;
+struct ase_transient_state_params {
+ std::vector<uint8_t> metadata;
+};
+
+constexpr uint16_t kCtpAseEntryMinLen = 3;
+struct ctp_ase_entry {
+ uint8_t ase_id;
+ uint8_t response_code;
+ uint8_t reason;
+};
+
+constexpr uint16_t kCtpNtfMinLen = 2;
+struct ctp_ntf {
+ uint8_t op;
+ std::vector<struct ctp_ase_entry> entries;
+};
+
+constexpr uint16_t kAseRspHdrMinLen = 2;
+struct ase_rsp_hdr {
+ uint8_t id;
+ uint8_t state;
+};
+
+constexpr uint8_t kCtpOpSize = 1;
+constexpr uint8_t kAseNumSize = 1;
+constexpr uint8_t kAseIdSize = 1;
+
+constexpr uint16_t kCtpCodecConfMinLen = 9;
+struct ctp_codec_conf {
+ uint8_t ase_id;
+ uint8_t target_latency;
+ uint8_t target_phy;
+ types::LeAudioCodecId codec_id;
+ types::LeAudioLc3Config codec_config;
+};
+
+constexpr uint16_t kCtpQosConfMinLen = 16;
+struct ctp_qos_conf {
+ uint8_t ase_id;
+ uint8_t cig;
+ uint8_t cis;
+ uint32_t sdu_interval;
+ uint8_t framing;
+ uint8_t phy;
+ uint16_t max_sdu;
+ uint8_t retrans_nb;
+ uint16_t max_transport_latency;
+ uint32_t pres_delay;
+};
+
+constexpr uint16_t kCtpEnableMinLen = 2;
+struct ctp_enable {
+ uint8_t ase_id;
+ std::vector<uint8_t> metadata;
+};
+
+constexpr uint16_t kCtpUpdateMetadataMinLen = 2;
+struct ctp_update_metadata {
+ uint8_t ase_id;
+ std::vector<uint8_t> metadata;
+};
+
+/* Device control and common functions */
+bool ParseAseStatusHeader(ase_rsp_hdr& rsp, uint16_t len, const uint8_t* value);
+bool ParseAseStatusCodecConfiguredStateParams(
+ struct ase_codec_configured_state_params& rsp, uint16_t len,
+ const uint8_t* value);
+bool ParseAseStatusQosConfiguredStateParams(
+ struct ase_qos_configured_state_params& rsp, uint16_t len,
+ const uint8_t* value);
+bool ParseAseStatusTransientStateParams(struct ase_transient_state_params& rsp,
+ uint16_t len, const uint8_t* value);
+bool ParseAseCtpNotification(struct ctp_ntf& ntf, uint16_t len,
+ const uint8_t* value);
+bool PrepareAseCtpCodecConfig(const std::vector<struct ctp_codec_conf>& confs,
+ std::vector<uint8_t>& value);
+bool PrepareAseCtpConfigQos(const std::vector<struct ctp_qos_conf>& confs,
+ std::vector<uint8_t>& value);
+bool PrepareAseCtpEnable(const std::vector<struct ctp_enable>& confs,
+ std::vector<uint8_t>& value);
+bool PrepareAseCtpAudioReceiverStartReady(const std::vector<uint8_t>& ids,
+ std::vector<uint8_t>& value);
+bool PrepareAseCtpDisable(const std::vector<uint8_t>& ids,
+ std::vector<uint8_t>& value);
+bool PrepareAseCtpAudioReceiverStopReady(const std::vector<uint8_t>& ids,
+ std::vector<uint8_t>& value);
+bool PrepareAseCtpUpdateMetadata(
+ const std::vector<struct ctp_update_metadata>& confs,
+ std::vector<uint8_t>& value);
+bool PrepareAseCtpRelease(const std::vector<uint8_t>& ids,
+ std::vector<uint8_t>& value);
+} // namespace ascs
+
+namespace pacs {
+
+constexpr uint16_t kAcsPacRecordMinLen = 7;
+constexpr uint8_t kAcsPacMetadataLenLen = 1;
+constexpr uint16_t kAcsPacDiscoverRspMinLen = 1;
+
+constexpr uint16_t kAudioLocationsRspMinLen = 4;
+
+constexpr uint16_t kAseAudioAvailRspMinLen = 4;
+struct acs_available_audio_contexts {
+ std::bitset<16> snk_avail_cont;
+ std::bitset<16> src_avail_cont;
+};
+
+constexpr uint16_t kAseAudioSuppContRspMinLen = 4;
+struct acs_supported_audio_contexts {
+ std::bitset<16> snk_supp_cont;
+ std::bitset<16> src_supp_cont;
+};
+
+bool ParsePac(std::vector<struct types::acs_ac_record>& pac_recs, uint16_t len,
+ const uint8_t* value);
+bool ParseAudioLocations(types::AudioLocations& audio_locations, uint16_t len,
+ const uint8_t* value);
+bool ParseAvailableAudioContexts(struct acs_available_audio_contexts& rsp,
+ uint16_t len, const uint8_t* value);
+bool ParseSupportedAudioContexts(struct acs_supported_audio_contexts& rsp,
+ uint16_t len, const uint8_t* value);
+} // namespace pacs
+} // namespace client_parser
+} // namespace le_audio
diff --git a/bta/le_audio/client_parser_test.cc b/bta/le_audio/client_parser_test.cc
new file mode 100644
index 000000000..2927792bc
--- /dev/null
+++ b/bta/le_audio/client_parser_test.cc
@@ -0,0 +1,1647 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+#include "client_parser.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "le_audio_types.h"
+
+namespace le_audio {
+namespace client_parser {
+namespace pacs {
+
+TEST(LeAudioClientParserTest, testParsePacInvalidLength) {
+ std::vector<struct types::acs_ac_record> pac_recs;
+
+ const uint8_t invalid_num_records[] = {0x01};
+ ASSERT_FALSE(
+ ParsePac(pac_recs, sizeof(invalid_num_records), invalid_num_records));
+
+ const uint8_t no_caps_len[] = {
+ // Num records
+ 0x01,
+ // Codec_ID
+ 0x01,
+ 0x02,
+ 0x03,
+ 0x04,
+ 0x05,
+ };
+ ASSERT_FALSE(ParsePac(pac_recs, sizeof(no_caps_len), no_caps_len));
+
+ const uint8_t no_metalen[] = {
+ // Num records
+ 0x01,
+ // Codec_ID
+ 0x01,
+ 0x02,
+ 0x03,
+ 0x04,
+ 0x05,
+ // Codec Spec. Caps. Len
+ 0x00,
+ };
+ ASSERT_FALSE(ParsePac(pac_recs, sizeof(no_metalen), no_metalen));
+}
+
+TEST(LeAudioClientParserTest, testParsePacEmpty) {
+ std::vector<struct types::acs_ac_record> pac_recs;
+ const uint8_t value[] = {0x00};
+
+ ASSERT_TRUE(ParsePac(pac_recs, sizeof(value), value));
+}
+
+TEST(LeAudioClientParserTest, testParsePacEmptyCapsEmptyMeta) {
+ std::vector<struct types::acs_ac_record> pac_recs;
+
+ const uint8_t value[] = {
+ // Num records
+ 0x01,
+ // Codec_ID
+ 0x01,
+ 0x03,
+ 0x02,
+ 0x05,
+ 0x04,
+ // Codec Spec. Caps. Len
+ 0x00,
+ // Metadata Length
+ 0x00,
+ };
+ ASSERT_TRUE(ParsePac(pac_recs, sizeof(value), value));
+
+ ASSERT_EQ(pac_recs.size(), 1u);
+ ASSERT_EQ(pac_recs[0].codec_id.coding_format, 0x01u);
+ ASSERT_EQ(pac_recs[0].codec_id.vendor_company_id, 0x0203u);
+ ASSERT_EQ(pac_recs[0].codec_id.vendor_codec_id, 0x0405u);
+}
+
+TEST(LeAudioClientParserTest, testParsePacInvalidCapsLen) {
+ std::vector<struct types::acs_ac_record> pac_recs;
+
+ const uint8_t bad_capslem[] = {
+ // Num records
+ 0x01,
+ // Codec_ID
+ 0x01,
+ 0x03,
+ 0x02,
+ 0x05,
+ 0x04,
+ // Codec Spec. Caps. Len
+ 0x05,
+ // Codec Spec. Caps.
+ 0x02, // [0].length,
+ 0x02, // [0].type,
+ 0x03, // [0].value[0]
+ 0x03, // [1].length
+ 0x03, // [1].type
+ 0x04, // [1].value[0]
+ 0x05, // [1].value[1]
+ // Metadata Length
+ 0x00,
+ };
+ ASSERT_FALSE(ParsePac(pac_recs, sizeof(bad_capslem), bad_capslem));
+
+ std::vector<struct types::acs_ac_record> pac_recs2;
+
+ const uint8_t bad_capslen2[] = {
+ // Num records
+ 0x01,
+ // Codec_ID
+ 0x01,
+ 0x03,
+ 0x02,
+ 0x05,
+ 0x04,
+ // Codec Spec. Caps. Len
+ 0x20,
+ // Codec Spec. Caps.
+ 0x02, // [0].length
+ 0x02, // [0].type
+ 0x03, // [0].value[0]
+ 0x03, // [1].length
+ 0x03, // [1].type
+ 0x04, // [1].value[0]
+ 0x05, // [1].value[1]
+ // Metadata Length
+ 0x00,
+ };
+ ASSERT_FALSE(ParsePac(pac_recs2, sizeof(bad_capslen2), bad_capslen2));
+}
+
+TEST(LeAudioClientParserTest, testParsePacInvalidCapsLtvLen) {
+ std::vector<struct types::acs_ac_record> pac_recs;
+
+ const uint8_t bad_ltv_len[] = {
+ // Num records
+ 0x01,
+ // Codec_ID
+ 0x01,
+ 0x03,
+ 0x02,
+ 0x05,
+ 0x04,
+ // Codec Spec. Caps. Len
+ 0x07,
+ // Codec Spec. Caps.
+ 0x02, // [0].length
+ 0x02, // [0].type
+ 0x03, // [0].value[0]
+ 0x06, // [1].bad_length
+ 0x03, // [1].type
+ 0x04, // [1].value[0]
+ 0x05, // [1].value[1]
+ // Metadata Length
+ 0x00,
+ };
+ ASSERT_FALSE(ParsePac(pac_recs, sizeof(bad_ltv_len), bad_ltv_len));
+
+ const uint8_t bad_ltv_len2[] = {
+ // Num records
+ 0x01,
+ // Codec_ID
+ 0x01,
+ 0x03,
+ 0x02,
+ 0x05,
+ 0x04,
+ // Codec Spec. Caps. Len
+ 0x07,
+ // Codec Spec. Caps.
+ 0x02, // [0].length
+ 0x02, // [0].type
+ 0x03, // [0].value[0]
+ 0x04, // [1].bad_length
+ 0x03, // [1].type
+ 0x04, // [1].value[0]
+ 0x05, // [1].value[1]
+ // Metadata Length
+ 0x00,
+ };
+ ASSERT_FALSE(ParsePac(pac_recs, sizeof(bad_ltv_len2), bad_ltv_len2));
+}
+
+TEST(LeAudioClientParserTest, testParsePacNullLtv) {
+ std::vector<struct types::acs_ac_record> pac_recs;
+
+ const uint8_t value[] = {
+ // Num records
+ 0x01,
+ // Codec_ID
+ 0x01,
+ 0x03,
+ 0x02,
+ 0x05,
+ 0x04,
+ // Codec Spec. Caps. Len
+ 0x0A,
+ // Codec Spec. Caps.
+ 0x02, // [0].length
+ 0x02, // [0].type
+ 0x03, // [0].value[0]
+ 0x03, // [1].length
+ 0x03, // [1].type
+ 0x04, // [1].value[0]
+ 0x05, // [1].value[1]
+ 0x01, // [2].length <-- a capability without a value
+ 0x04, // [2].type
+ 0x00, // [3]length <-- this seems possible although useless
+ // Metadata Length
+ 0x00,
+ };
+ ASSERT_TRUE(ParsePac(pac_recs, sizeof(value), value));
+
+ ASSERT_EQ(pac_recs.size(), 1u);
+ ASSERT_EQ(pac_recs[0].codec_id.coding_format, 0x01u);
+ ASSERT_EQ(pac_recs[0].codec_id.vendor_company_id, 0x0203u);
+ ASSERT_EQ(pac_recs[0].codec_id.vendor_codec_id, 0x0405u);
+
+ auto codec_spec_caps = pac_recs[0].codec_spec_caps.Values();
+ ASSERT_EQ(codec_spec_caps.size(), 3u);
+ ASSERT_EQ(codec_spec_caps.count(0x02u), 1u);
+ ASSERT_EQ(codec_spec_caps[0x02u].size(), 1u);
+ ASSERT_EQ(codec_spec_caps[0x02u][0], 0x03u);
+ ASSERT_EQ(codec_spec_caps.count(0x03u), 1u);
+ ASSERT_EQ(codec_spec_caps[0x03u].size(), 2u);
+ ASSERT_EQ(codec_spec_caps[0x03u][0], 0x04u);
+ ASSERT_EQ(codec_spec_caps[0x03u][1], 0x05u);
+ ASSERT_EQ(codec_spec_caps.count(0x04u), 1u);
+ ASSERT_EQ(codec_spec_caps[0x04u].size(), 0u);
+}
+
+TEST(LeAudioClientParserTest, testParsePacEmptyMeta) {
+ std::vector<struct types::acs_ac_record> pac_recs;
+
+ const uint8_t value[] = {
+ // Num records
+ 0x01,
+ // Codec_ID
+ 0x01,
+ 0x03,
+ 0x02,
+ 0x05,
+ 0x04,
+ // Codec Spec. Caps. Len
+ 0x07,
+ // Codec Spec. Caps.
+ 0x02, // [0].length
+ 0x02, // [0].type
+ 0x03, // [0].value[0]
+ 0x03, // [1].length
+ 0x03, // [1].type
+ 0x04, // [1].value[0]
+ 0x05, // [1].value[1]
+ // Metadata Length
+ 0x00,
+ };
+ ASSERT_TRUE(ParsePac(pac_recs, sizeof(value), value));
+
+ ASSERT_EQ(pac_recs.size(), 1u);
+ ASSERT_EQ(pac_recs[0].codec_id.coding_format, 0x01u);
+ ASSERT_EQ(pac_recs[0].codec_id.vendor_company_id, 0x0203u);
+ ASSERT_EQ(pac_recs[0].codec_id.vendor_codec_id, 0x0405u);
+
+ auto codec_spec_caps = pac_recs[0].codec_spec_caps.Values();
+ ASSERT_EQ(codec_spec_caps.size(), 2u);
+ ASSERT_EQ(codec_spec_caps.count(0x02u), 1u);
+ ASSERT_EQ(codec_spec_caps[0x02u].size(), 1u);
+ ASSERT_EQ(codec_spec_caps[0x02u][0], 0x03u);
+ ASSERT_EQ(codec_spec_caps.count(0x03u), 1u);
+ ASSERT_EQ(codec_spec_caps[0x03u].size(), 2u);
+ ASSERT_EQ(codec_spec_caps[0x03u][0], 0x04u);
+ ASSERT_EQ(codec_spec_caps[0x03u][1], 0x05u);
+}
+
+TEST(LeAudioClientParserTest, testParsePacInvalidMetaLength) {
+ std::vector<struct types::acs_ac_record> pac_recs;
+
+ const uint8_t value[] = {
+ // Num records
+ 0x01,
+ // Codec_ID
+ 0x01, 0x03, 0x02, 0x05, 0x04,
+ // Codec Spec. Caps. Len
+ 0x07,
+ // Codec Spec. Caps.
+ 0x02, // [0].length
+ 0x02, // [0].type
+ 0x03, // [0].value[0]
+ 0x03, // [1].length
+ 0x03, // [1].type
+ 0x04, // [1].value[0]
+ 0x05, // [1].value[1]
+ // Metadata Length
+ 0x05,
+ // Metadata
+ 0x03, // [0].length
+ 0x02, // [0].type
+ 0x01, // [0].value[0]
+ 0x00, // [0].value[1]
+ };
+ ASSERT_FALSE(ParsePac(pac_recs, sizeof(value), value));
+}
+
+TEST(LeAudioClientParserTest, testParsePacValidMeta) {
+ std::vector<struct types::acs_ac_record> pac_recs;
+
+ const uint8_t value[] = {
+ // Num records
+ 0x01,
+ // Codec_ID
+ 0x01, 0x03, 0x02, 0x05, 0x04,
+ // Codec Spec. Caps. Len
+ 0x07,
+ // Codec Spec. Caps.
+ 0x02, // [0].length
+ 0x02, // [0].type
+ 0x03, // [0].value[0]
+ 0x03, // [1].length
+ 0x03, // [1].type
+ 0x04, // [1].value[0]
+ 0x05, // [1].value[1]
+ // Metadata Length
+ 0x04,
+ // Metadata
+ 0x03, // [0].length
+ 0x02, // [0].type
+ 0x01, // [0].value[0]
+ 0x00, // [0].value[1]
+ };
+ ASSERT_TRUE(ParsePac(pac_recs, sizeof(value), value));
+
+ ASSERT_EQ(pac_recs.size(), 1u);
+ ASSERT_EQ(pac_recs[0].codec_id.coding_format, 0x01u);
+ ASSERT_EQ(pac_recs[0].codec_id.vendor_company_id, 0x0203u);
+ ASSERT_EQ(pac_recs[0].codec_id.vendor_codec_id, 0x0405u);
+
+ auto codec_spec_caps = pac_recs[0].codec_spec_caps.Values();
+ ASSERT_EQ(codec_spec_caps.size(), 2u);
+ ASSERT_EQ(codec_spec_caps.count(0x02u), 1u);
+ ASSERT_EQ(codec_spec_caps[0x02u].size(), 1u);
+ ASSERT_EQ(codec_spec_caps[0x02u][0], 0x03u);
+ ASSERT_EQ(codec_spec_caps.count(0x03u), 1u);
+ ASSERT_EQ(codec_spec_caps[0x03u].size(), 2u);
+ ASSERT_EQ(codec_spec_caps[0x03u][0], 0x04u);
+ ASSERT_EQ(codec_spec_caps[0x03u][1], 0x05u);
+
+ ASSERT_EQ(pac_recs[0].metadata.size(), 4u);
+ ASSERT_EQ(pac_recs[0].metadata[0], 0x03u);
+ ASSERT_EQ(pac_recs[0].metadata[1], 0x02u);
+ ASSERT_EQ(pac_recs[0].metadata[2], 0x01u);
+ ASSERT_EQ(pac_recs[0].metadata[3], 0x00u);
+}
+
+TEST(LeAudioClientParserTest, testParsePacInvalidNumRecords) {
+ std::vector<struct types::acs_ac_record> pac_recs;
+
+ const uint8_t value[] = {
+ // Num records
+ 0x02,
+ // Codec_ID
+ 0x01, 0x03, 0x02, 0x05, 0x04,
+ // Codec Spec. Caps. Len
+ 0x07,
+ // Codec Spec. Caps.
+ 0x02, // [0].length
+ 0x02, // [0].type
+ 0x03, // [0].value[0]
+ 0x03, // [1].length
+ 0x03, // [1].type
+ 0x04, // [1].value[0]
+ 0x05, // [1].value[1]
+ // Metadata Length
+ 0x04,
+ // Metadata
+ 0x03, // [0].length
+ 0x02, // [0].type
+ 0x01, // [0].value[0]
+ 0x00, // [0].value[1]
+ };
+ ASSERT_FALSE(ParsePac(pac_recs, sizeof(value), value));
+}
+
+TEST(LeAudioClientParserTest, testParsePacMultipleRecords) {
+ std::vector<struct types::acs_ac_record> pac_recs;
+
+ const uint8_t value[] = {
+ // Num records
+ 0x03,
+ // Codec_ID
+ 0x01, 0x03, 0x02, 0x05, 0x04,
+ // Codec Spec. Caps. Len
+ 0x00,
+ // Metadata Length
+ 0x00,
+ // Codec_ID
+ 0x06, 0x08, 0x07, 0x0A, 0x09,
+ // Codec Spec. Caps. Len
+ 0x03,
+ // Codec Spec. Caps.
+ 0x02, // [0].length
+ 0x02, // [0].type
+ 0x03, // [0].value[0]
+ // Metadata Length
+ 0x04,
+ // Metadata
+ 0x03, // [0].length
+ 0x02, // [0].type
+ 0x01, // [0].value[0]
+ 0x00, // [0].value[1],
+ // Codec_ID
+ 0x11, 0x13, 0x12, 0x15, 0x14,
+ // Codec Spec. Caps. Len
+ 0x07,
+ // Codec Spec. Caps.
+ 0x02, // [0].length
+ 0x12, // [0].type
+ 0x13, // [0].value[0]
+ 0x03, // [1].length
+ 0x13, // [1].type
+ 0x14, // [1].value[0]
+ 0x15, // [1].value[1]
+ // Metadata Length
+ 0x04,
+ // Metadata
+ 0x03, // [0].length
+ 0x12, // [0].type
+ 0x11, // [0].value[0]
+ 0x10, // [0].value[1]
+ };
+ ASSERT_TRUE(ParsePac(pac_recs, sizeof(value), value));
+ ASSERT_EQ(pac_recs.size(), 3u);
+
+ // Verify 1st record
+ auto& record0 = pac_recs[0];
+
+ ASSERT_EQ(record0.codec_id.coding_format, 0x01u);
+ ASSERT_EQ(record0.codec_id.vendor_company_id, 0x0203u);
+ ASSERT_EQ(record0.codec_id.vendor_codec_id, 0x0405u);
+ ASSERT_EQ(record0.codec_spec_caps.Size(), 0u);
+ ASSERT_EQ(record0.metadata.size(), 0u);
+
+ // Verify 2nd record
+ auto& record1 = pac_recs[1];
+
+ ASSERT_EQ(record1.codec_id.coding_format, 0x06u);
+ ASSERT_EQ(record1.codec_id.vendor_company_id, 0x0708u);
+ ASSERT_EQ(record1.codec_id.vendor_codec_id, 0x090Au);
+
+ auto codec_spec_caps1 = record1.codec_spec_caps.Values();
+ ASSERT_EQ(codec_spec_caps1.size(), 1u);
+ ASSERT_EQ(codec_spec_caps1.count(0x02u), 1u);
+ ASSERT_EQ(codec_spec_caps1[0x02u].size(), 1u);
+ ASSERT_EQ(codec_spec_caps1[0x02u][0], 0x03u);
+
+ ASSERT_EQ(record1.metadata.size(), 4u);
+ ASSERT_EQ(record1.metadata[0], 0x03u);
+ ASSERT_EQ(record1.metadata[1], 0x02u);
+ ASSERT_EQ(record1.metadata[2], 0x01u);
+ ASSERT_EQ(record1.metadata[3], 0x00u);
+
+ // Verify 3rd record
+ auto& record2 = pac_recs[2];
+
+ ASSERT_EQ(record2.codec_id.coding_format, 0x11u);
+ ASSERT_EQ(record2.codec_id.vendor_company_id, 0x1213u);
+ ASSERT_EQ(record2.codec_id.vendor_codec_id, 0x1415u);
+
+ auto codec_spec_caps2 = record2.codec_spec_caps.Values();
+ ASSERT_EQ(codec_spec_caps2.size(), 2u);
+ ASSERT_EQ(codec_spec_caps2.count(0x12u), 1u);
+ ASSERT_EQ(codec_spec_caps2[0x12u].size(), 1u);
+ ASSERT_EQ(codec_spec_caps2[0x12u][0], 0x13u);
+ ASSERT_EQ(codec_spec_caps2.count(0x13u), 1u);
+ ASSERT_EQ(codec_spec_caps2[0x13u].size(), 2u);
+ ASSERT_EQ(codec_spec_caps2[0x13u][0], 0x14u);
+ ASSERT_EQ(codec_spec_caps2[0x13u][1], 0x15u);
+
+ ASSERT_EQ(record2.metadata.size(), 4u);
+ ASSERT_EQ(record2.metadata[0], 0x03u);
+ ASSERT_EQ(record2.metadata[1], 0x12u);
+ ASSERT_EQ(record2.metadata[2], 0x11u);
+ ASSERT_EQ(record2.metadata[3], 0x10u);
+}
+
+TEST(LeAudioClientParserTest, testParseAudioLocationsInvalidLength) {
+ types::AudioLocations locations =
+ codec_spec_conf::kLeAudioLocationMonoUnspecified;
+ const uint8_t value1[] = {
+ 0x01,
+ 0x02,
+ 0x03,
+ };
+ ParseAudioLocations(locations, sizeof(value1), value1);
+ ASSERT_EQ(locations, 0u);
+
+ const uint8_t value2[] = {0x01, 0x02, 0x03, 0x04, 0x05};
+ ParseAudioLocations(locations, sizeof(value2), value2);
+ ASSERT_EQ(locations, 0u);
+}
+
+TEST(LeAudioClientParserTest, testParseAudioLocations) {
+ types::AudioLocations locations =
+ codec_spec_conf::kLeAudioLocationMonoUnspecified;
+ const uint8_t value1[] = {0x01, 0x02, 0x03, 0x04};
+ ParseAudioLocations(locations, sizeof(value1), value1);
+ ASSERT_EQ(locations, 0x04030201u);
+}
+
+TEST(LeAudioClientParserTest, testParseAvailableAudioContextsInvalidLength) {
+ acs_available_audio_contexts avail_contexts;
+ const uint8_t value1[] = {
+ // Sink available contexts
+ 0x01, 0x02,
+ // Missing Source available contexts
+ };
+
+ ParseAvailableAudioContexts(avail_contexts, sizeof(value1), value1);
+ ASSERT_EQ(avail_contexts.snk_avail_cont, 0u);
+ ASSERT_EQ(avail_contexts.src_avail_cont, 0u);
+}
+
+TEST(LeAudioClientParserTest, testParseAvailableAudioContexts) {
+ acs_available_audio_contexts avail_contexts;
+ const uint8_t value1[] = {
+ // Sink available contexts
+ 0x01,
+ 0x02,
+ // Source available contexts
+ 0x03,
+ 0x04,
+ };
+
+ ParseAvailableAudioContexts(avail_contexts, sizeof(value1), value1);
+ ASSERT_EQ(avail_contexts.snk_avail_cont, 0x0201u);
+ ASSERT_EQ(avail_contexts.src_avail_cont, 0x0403u);
+}
+
+TEST(LeAudioClientParserTest, testParseSupportedAudioContextsInvalidLength) {
+ acs_supported_audio_contexts supp_contexts;
+ const uint8_t value1[] = {
+ // Sink supported contexts
+ 0x01, 0x02,
+ // Missing Source supported contexts
+ };
+
+ ParseSupportedAudioContexts(supp_contexts, sizeof(value1), value1);
+ ASSERT_EQ(supp_contexts.snk_supp_cont, 0u);
+ ASSERT_EQ(supp_contexts.src_supp_cont, 0u);
+}
+
+TEST(LeAudioClientParserTest, testParseSupportedAudioContexts) {
+ acs_supported_audio_contexts supp_contexts;
+ const uint8_t value1[] = {
+ // Sink supported contexts
+ 0x01,
+ 0x02,
+ // Source supported contexts
+ 0x03,
+ 0x04,
+ };
+
+ ParseSupportedAudioContexts(supp_contexts, sizeof(value1), value1);
+ ASSERT_EQ(supp_contexts.snk_supp_cont, 0x0201u);
+ ASSERT_EQ(supp_contexts.src_supp_cont, 0x0403u);
+}
+
+} // namespace pacs
+
+namespace ascs {
+
+TEST(LeAudioClientParserTest, testParseAseStatusHeaderInvalidLength) {
+ ase_rsp_hdr arh;
+ const uint8_t value1[] = {
+ // Ase ID
+ 0x01,
+ // ASE State is missing here
+ };
+ ASSERT_FALSE(ParseAseStatusHeader(arh, sizeof(value1), value1));
+}
+
+TEST(LeAudioClientParserTest, testParseAseStatusHeader) {
+ ase_rsp_hdr arh;
+ const uint8_t value1[] = {
+ // Ase ID
+ 0x01,
+ // ASE State
+ 0x00, // 'Idle' state
+ // No additional ASE Params for the 'Idle' state
+ };
+ ASSERT_TRUE(ParseAseStatusHeader(arh, sizeof(value1), value1));
+ ASSERT_EQ(arh.id, 0x01u);
+ ASSERT_EQ(arh.state, 0x00u);
+
+ const uint8_t value2[] = {
+ // Ase ID
+ 0x02,
+ // ASE State
+ 0x04, // 'Streaming' state
+ // Additional ASE Params for the 'Streaming' state
+ // Metadata Len
+ 0x03,
+ // Metadata
+ 0x03, // [0].length
+ 0x02, // [0].type
+ 0x01, // [0].value[0]
+ 0x00, // [0].value[1]
+ };
+ ASSERT_TRUE(ParseAseStatusHeader(arh, sizeof(value2), value2));
+ ASSERT_EQ(arh.id, 0x02u);
+ ASSERT_EQ(arh.state, 0x04u);
+ // Currently additional state parameters are not handled
+}
+
+TEST(LeAudioClientParserTest,
+ testParseAseStatusCodecConfiguredStateParamsInvalidLength) {
+ ase_codec_configured_state_params codec_configured_state_params;
+ const uint8_t value1[] = {
+ // Ase ID
+ 0x02,
+ // ASE State
+ 0x01, // 'Codec Configured' state
+ // Framing
+ 0x01, // Unframed
+ // Peferred PHY
+ 0x02, // 2M PHY
+ // Preferred retransimssion Num.
+ 0x04,
+ // Max transport Latency
+ 0x05, 0x00,
+ // Pressentation delay min.
+ 0x00, 0x01, 0x02, 0x03,
+ // Pressentation delay max.
+ 0x00, 0x01, 0x02, 0x03,
+ // Preferred presentation delay min.
+ 0x01, 0x02, 0x03,
+ // Preferred presentation delay max.
+ 0x01, 0x02, 0x03,
+ // Codec ID
+ 0x01, 0x02, 0x03, 0x04, 0x05,
+ // Missing Codec spec. conf. length
+ };
+
+ ASSERT_FALSE(ParseAseStatusCodecConfiguredStateParams(
+ codec_configured_state_params, sizeof(value1) - 2, value1 + 2));
+}
+
+TEST(LeAudioClientParserTest, testParseAseStatusCodecConfiguredStateParams) {
+ ase_codec_configured_state_params codec_configured_state_params;
+ const uint8_t value1[] = {
+ // Ase ID
+ 0x01,
+ // ASE State
+ 0x01, // 'Codec Configured' state
+ // Framing
+ 0x01, // Unframed
+ // Peferred PHY
+ 0x02, // 2M PHY
+ // Preferred retransimssion Num.
+ 0x04,
+ // Max transport Latency
+ 0x05,
+ 0x00,
+ // Pressentation delay min.
+ 0x00,
+ 0x01,
+ 0x02,
+ // Pressentation delay max.
+ 0x10,
+ 0x11,
+ 0x12,
+ // Preferred presentation delay min.
+ 0x01,
+ 0x02,
+ 0x03,
+ // Preferred presentation delay max.
+ 0x09,
+ 0x10,
+ 0x11,
+ // Codec ID
+ 0x01,
+ 0x02,
+ 0x03,
+ 0x04,
+ 0x05,
+ // Codec spec. conf. length
+ 0x00,
+ };
+
+ // State additional parameters are right after the ASE ID and state bytes
+ ASSERT_TRUE(ParseAseStatusCodecConfiguredStateParams(
+ codec_configured_state_params, sizeof(value1) - 2, value1 + 2));
+ ASSERT_EQ(codec_configured_state_params.framing, 0x01u);
+ ASSERT_EQ(codec_configured_state_params.preferred_phy, 0x02u);
+ ASSERT_EQ(codec_configured_state_params.preferred_retrans_nb, 0x04u);
+ ASSERT_EQ(codec_configured_state_params.max_transport_latency, 0x0005u);
+ ASSERT_EQ(codec_configured_state_params.pres_delay_min, 0x020100u);
+ ASSERT_EQ(codec_configured_state_params.pres_delay_max, 0x121110u);
+ ASSERT_EQ(codec_configured_state_params.preferred_pres_delay_min, 0x030201u);
+ ASSERT_EQ(codec_configured_state_params.preferred_pres_delay_max, 0x111009u);
+ ASSERT_EQ(codec_configured_state_params.codec_id.coding_format, 0x01u);
+ ASSERT_EQ(codec_configured_state_params.codec_id.vendor_company_id, 0x0302u);
+ ASSERT_EQ(codec_configured_state_params.codec_id.vendor_codec_id, 0x0504u);
+ ASSERT_EQ(codec_configured_state_params.codec_spec_conf.size(), 0u);
+
+ const uint8_t value2[] = {
+ // Ase ID
+ 0x02,
+ // ASE State
+ 0x01, // 'Codec Configured' state
+ // Framing
+ 0x01, // Unframed
+ // Peferred PHY
+ 0x02, // 2M PHY
+ // Preferred retransimssion Num.
+ 0x04,
+ // Max transport Latency
+ 0x05,
+ 0x00,
+ // Pressentation delay min.
+ 0x00,
+ 0x01,
+ 0x02,
+ // Pressentation delay max.
+ 0x10,
+ 0x11,
+ 0x12,
+ // Preferred presentation delay min.
+ 0x01,
+ 0x02,
+ 0x03,
+ // Preferred presentation delay max.
+ 0x09,
+ 0x10,
+ 0x11,
+ // Codec ID
+ 0x01,
+ 0x02,
+ 0x03,
+ 0x04,
+ 0x05,
+ // Codec spec. conf. length
+ 0x05,
+ // Codec spec. conf.
+ 0x0A,
+ 0x0B,
+ 0x0C,
+ 0x0D,
+ 0x0E,
+ };
+
+ // State additional parameters are right after the ASE ID and state bytes
+ ASSERT_TRUE(ParseAseStatusCodecConfiguredStateParams(
+ codec_configured_state_params, sizeof(value2) - 2, value2 + 2));
+ ASSERT_EQ(codec_configured_state_params.framing, 0x01u);
+ ASSERT_EQ(codec_configured_state_params.preferred_phy, 0x02u);
+ ASSERT_EQ(codec_configured_state_params.preferred_retrans_nb, 0x04u);
+ ASSERT_EQ(codec_configured_state_params.max_transport_latency, 0x0005u);
+ ASSERT_EQ(codec_configured_state_params.pres_delay_min, 0x020100u);
+ ASSERT_EQ(codec_configured_state_params.pres_delay_max, 0x121110u);
+ ASSERT_EQ(codec_configured_state_params.preferred_pres_delay_min, 0x030201u);
+ ASSERT_EQ(codec_configured_state_params.preferred_pres_delay_max, 0x111009u);
+ ASSERT_EQ(codec_configured_state_params.codec_id.coding_format, 0x01u);
+ ASSERT_EQ(codec_configured_state_params.codec_id.vendor_company_id, 0x0302u);
+ ASSERT_EQ(codec_configured_state_params.codec_id.vendor_codec_id, 0x0504u);
+ ASSERT_EQ(codec_configured_state_params.codec_spec_conf.size(), 5u);
+ ASSERT_EQ(codec_configured_state_params.codec_spec_conf[0], 0x0Au);
+ ASSERT_EQ(codec_configured_state_params.codec_spec_conf[1], 0x0Bu);
+ ASSERT_EQ(codec_configured_state_params.codec_spec_conf[2], 0x0Cu);
+ ASSERT_EQ(codec_configured_state_params.codec_spec_conf[3], 0x0Du);
+ ASSERT_EQ(codec_configured_state_params.codec_spec_conf[4], 0x0Eu);
+}
+
+TEST(LeAudioClientParserTest,
+ testParseAseStatusQosConfiguredStateParamsInvalidLength) {
+ struct ase_qos_configured_state_params rsp {
+ .cig_id = 0, .cis_id = 0
+ };
+ const uint8_t value1[] = {
+ // Ase ID
+ 0x01,
+ // ASE State
+ 0x02, // 'QoS Configured' state
+ 0x03, // CIG_ID
+ 0x04, // CIS_ID
+ };
+
+ ParseAseStatusQosConfiguredStateParams(rsp, sizeof(value1) - 2, value1 + 2);
+ ASSERT_EQ(rsp.cig_id, 0);
+ ASSERT_EQ(rsp.cis_id, 0);
+
+ const uint8_t value2[] = {
+ // Ase ID
+ 0x01,
+ // ASE State
+ 0x02, // 'QoS Configured' state
+ // CIG_ID
+ 0x03,
+ // CIS_ID
+ 0x04,
+ // SDU Interval
+ 0x05, 0x06, 0x07,
+ // Framing
+ 0x01,
+ // PHY
+ 0x02,
+ // Max SDU
+ 0x08, 0x09,
+ // Retransmission Num.
+ 0x0A,
+ // Max Transport Latency
+ 0x0B, 0x0C,
+ // Presentation Delay
+ 0x0D, 0x0E,
+ // Missing Byte
+ };
+
+ ParseAseStatusQosConfiguredStateParams(rsp, sizeof(value2) - 2, value2 + 2);
+ ASSERT_EQ(rsp.cig_id, 0);
+ ASSERT_EQ(rsp.cis_id, 0);
+}
+
+TEST(LeAudioClientParserTest, testParseAseStatusQosConfiguredStateParams) {
+ struct ase_qos_configured_state_params rsp;
+ const uint8_t value[] = {
+ // Ase ID
+ 0x01,
+ // ASE State - 'QoS Configured'
+ 0x02,
+ // CIG_ID
+ 0x03,
+ // CIS_ID
+ 0x04,
+ // SDU Interval
+ 0x05,
+ 0x06,
+ 0x07,
+ // Framing
+ 0x01,
+ // PHY
+ 0x02,
+ // Max SDU
+ 0x18,
+ 0x19,
+ // Retransmission Num.
+ 0x1A,
+ // Max Transport Latency
+ 0x1B,
+ 0x1C,
+ // Presentation Delay
+ 0x1D,
+ 0x1E,
+ 0x1F,
+ };
+
+ ParseAseStatusQosConfiguredStateParams(rsp, sizeof(value) - 2, value + 2);
+ ASSERT_EQ(rsp.cig_id, 0x03u);
+ ASSERT_EQ(rsp.cis_id, 0x04u);
+ ASSERT_EQ(rsp.sdu_interval, 0x070605u);
+ ASSERT_EQ(rsp.framing, 0x01u);
+ ASSERT_EQ(rsp.phy, 0x02u);
+ ASSERT_EQ(rsp.max_sdu, 0x1918u);
+ ASSERT_EQ(rsp.retrans_nb, 0x1Au);
+ ASSERT_EQ(rsp.max_transport_latency, 0x1C1Bu);
+ ASSERT_EQ(rsp.pres_delay, 0x1F1E1Du);
+}
+
+TEST(LeAudioClientParserTest,
+ testParseAseStatusTransientStateParamsInvalidLength) {
+ ase_transient_state_params params;
+ const uint8_t value1[] = {
+ // Ase ID
+ 0x01,
+ // ASE State
+ 0x03, // 'Enabling' state
+ // missing Metadata length
+ // missing Metadata
+ };
+ ParseAseStatusTransientStateParams(params, sizeof(value1) - 2, value1 + 2);
+}
+
+TEST(LeAudioClientParserTest, testParseAseStatusTransientStateParams) {
+ ase_transient_state_params params;
+ const uint8_t value1[] = {
+ // Ase ID
+ 0x01,
+ // ASE State
+ 0x03, // 'Enabling' state
+ // Metadata length
+ 0x00,
+ };
+ ParseAseStatusTransientStateParams(params, sizeof(value1) - 2, value1 + 2);
+ ASSERT_EQ(params.metadata.size(), 0u);
+
+ const uint8_t value2[] = {
+ // Ase ID
+ 0x01,
+ // ASE State
+ 0x03, // 'Enabling' state
+ // Metadata length
+ 0x03,
+ // Metadata
+ 0x02, // [0].length
+ 0x01, // [0].type
+ 0x00, // [0].value[0]
+ };
+ ParseAseStatusTransientStateParams(params, sizeof(value2) - 2, value2 + 2);
+
+ ASSERT_EQ(params.metadata.size(), 3u);
+ ASSERT_EQ(params.metadata[0], 0x02u);
+ ASSERT_EQ(params.metadata[1], 0x01u);
+ ASSERT_EQ(params.metadata[2], 0x00u);
+}
+
+TEST(LeAudioClientParserTest, testParseAseCtpNotificationInvalidLength) {
+ ctp_ntf ntf;
+ const uint8_t value1[] = {
+ // Opcode
+ 0x01,
+ // Number of ASEs
+ 0x02,
+ // ASE ID
+ 0x01,
+ // Response Code
+ 0x01,
+ // Reason
+ 0x01,
+ // ASE ID
+ 0x02,
+ // Response Code
+ 0x02,
+ // Missing Reason
+ };
+ ParseAseCtpNotification(ntf, sizeof(value1), value1);
+
+ // In case of invalid payload at least we get the opcode
+ ASSERT_EQ(ntf.op, 0x01u);
+ ASSERT_EQ(ntf.entries.size(), 0u);
+
+ const uint8_t value2[] = {
+ // Opcode
+ 0x01,
+ // Missing Number of ASEs
+ // Missing ASE ID
+ // Missing Response Code
+ // Missing Reason
+ // Missing ASE ID
+ // Missing Response Code
+ // Missing Reason
+ };
+ ntf.entries.clear();
+ ParseAseCtpNotification(ntf, sizeof(value2), value2);
+
+ // In case of invalid payload at least we get the opcode
+ ASSERT_EQ(ntf.op, 0x01u);
+ ASSERT_EQ(ntf.entries.size(), 0u);
+
+ const uint8_t value3[] = {
+ // Opcode
+ 0x01,
+ // Number of ASEs
+ 0x03,
+ // ASE ID
+ 0x01,
+ // Response Code
+ 0x01,
+ // Reason
+ 0x01,
+ // ASE ID
+ 0x02,
+ // Response Code
+ 0x02,
+ // Reason
+ 0x03,
+ // Missing the entire ASE entry
+ };
+
+ ntf.entries.clear();
+ ParseAseCtpNotification(ntf, sizeof(value3), value3);
+ // In case of invalid payload at least we get the opcode
+ ASSERT_EQ(ntf.op, 0x01u);
+ ASSERT_EQ(ntf.entries.size(), 0u);
+}
+
+TEST(LeAudioClientParserTest, testParseAseCtpNotification) {
+ ctp_ntf ntf;
+ const uint8_t value1[] = {
+ // Opcode
+ 0x01,
+ // Number of ASEs
+ 0x02,
+ // ASE ID
+ 0x01,
+ // Response Code
+ 0x01,
+ // Reason
+ 0x01,
+ // ASE ID
+ 0x03,
+ // Response Code
+ 0x02,
+ // Reason
+ 0x03,
+ };
+ ParseAseCtpNotification(ntf, sizeof(value1), value1);
+
+ ASSERT_EQ(ntf.op, 0x01u);
+ ASSERT_EQ(ntf.entries.size(), 2u);
+ ASSERT_EQ(ntf.entries[0].ase_id, 0x01u);
+ ASSERT_EQ(ntf.entries[0].response_code, 0x01u);
+ ASSERT_EQ(ntf.entries[0].reason, 0x01);
+ ASSERT_EQ(ntf.entries[1].ase_id, 0x03u);
+ ASSERT_EQ(ntf.entries[1].response_code, 0x02u);
+ ASSERT_EQ(ntf.entries[1].reason, 0x03);
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpCodecConfigEmpty) {
+ std::vector<struct ctp_codec_conf> confs;
+ std::vector<uint8_t> value;
+
+ PrepareAseCtpCodecConfig(confs, value);
+
+ ASSERT_EQ(value.size(), 0u);
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpCodecConfigSingle) {
+ std::vector<struct ctp_codec_conf> confs;
+ std::vector<uint8_t> value;
+
+ types::LeAudioCodecId codec_id{.coding_format = 0x06,
+ .vendor_company_id = 0x0203,
+ .vendor_codec_id = 0x0405};
+ types::LeAudioLc3Config codec_conf{.sampling_frequency = 0x10,
+ .frame_duration = 0x03,
+ .audio_channel_allocation = 0x04050607,
+ .octets_per_codec_frame = 0x0203};
+
+ confs.push_back(ctp_codec_conf{
+ .ase_id = 0x05,
+ .target_latency = 0x03,
+ .target_phy = 0x02,
+ .codec_id = codec_id,
+ .codec_config = codec_conf,
+ });
+ PrepareAseCtpCodecConfig(confs, value);
+
+ uint8_t i = 0;
+ ASSERT_NE(value.size(), 0u);
+ ASSERT_EQ(value[i++], 0x01); // Config Codec Opcode
+ ASSERT_EQ(value[i++], 0x01); // Number of ASEs
+ ASSERT_EQ(value[i++], 0x05); // ASE[0] ASE ID
+ ASSERT_EQ(value[i++], 0x03); // ASE[0] Target Latency
+ ASSERT_EQ(value[i++], 0x02); // ASE[0] Target Phy
+ ASSERT_EQ(value[i++], 0x06); // ASE[0].CodecID Coding Format
+ ASSERT_EQ(value[i++], 0x03); // ASE[0].CodecID Company ID LSB
+ ASSERT_EQ(value[i++], 0x02); // ASE[0].CodecID Company ID MSB
+ ASSERT_EQ(value[i++], 0x05); // ASE[0].CodecID Codec ID LSB
+ ASSERT_EQ(value[i++], 0x04); // ASE[0].CodecID Codec ID MSB
+
+ // ASE[0].Codec Spec. Conf. Length - LC3 specific
+ ASSERT_EQ(value[i++], 8 + 8); // * 4*2 bytes for 4 LTV types and lengths + 8
+ // bytes for the values
+ ASSERT_EQ(value[i++], 0x02); // Sampling Freq. Length
+ ASSERT_EQ(value[i++], 0x01); // Sampling Freq. Type
+ ASSERT_EQ(value[i++], 0x10); // Sampling Freq. Value
+ ASSERT_EQ(value[i++], 0x02); // Frame Duration. Length
+ ASSERT_EQ(value[i++], 0x02); // Frame Duration. Type
+ ASSERT_EQ(value[i++], 0x03); // Frame Duration. Value
+ ASSERT_EQ(value[i++], 0x05); // Audio Channel Allocations Length
+ ASSERT_EQ(value[i++], 0x03); // Audio Channel Allocations Type
+ ASSERT_EQ(value[i++], 0x07); // Audio Channel Allocations Value[0]
+ ASSERT_EQ(value[i++], 0x06); // Audio Channel Allocations Value[1]
+ ASSERT_EQ(value[i++], 0x05); // Audio Channel Allocations Value[2]
+ ASSERT_EQ(value[i++], 0x04); // Audio Channel Allocations Value[3]
+ ASSERT_EQ(value[i++], 0x03); // Octets Per Frame Length
+ ASSERT_EQ(value[i++], 0x04); // Octets Per Frame Type
+ ASSERT_EQ(value[i++], 0x03); // Octets Per Frame Value[0]
+ ASSERT_EQ(value[i++], 0x02); // Octets Per Frame Value[1]
+ ASSERT_EQ(value.size(), i);
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpCodecConfigMultiple) {
+ std::vector<struct ctp_codec_conf> confs;
+ std::vector<uint8_t> value;
+
+ types::LeAudioCodecId codec_id{.coding_format = 0x06,
+ .vendor_company_id = 0x0203,
+ .vendor_codec_id = 0x0405};
+ types::LeAudioLc3Config codec_conf{.sampling_frequency = 0x10,
+ .frame_duration = 0x03,
+ .audio_channel_allocation = 0x04050607,
+ .octets_per_codec_frame = 0x0203};
+
+ confs.push_back(ctp_codec_conf{
+ .ase_id = 0x05,
+ .target_latency = 0x03,
+ .target_phy = 0x02,
+ .codec_id = codec_id,
+ .codec_config = codec_conf,
+ });
+ PrepareAseCtpCodecConfig(confs, value);
+
+ uint8_t i = 0;
+ ASSERT_NE(value.size(), 0u);
+ ASSERT_EQ(value[i++], 0x01); // Config Codec Opcode
+ ASSERT_EQ(value[i++], 0x01); // Number of ASEs
+ ASSERT_EQ(value[i++], 0x05); // ASE[0] ASE ID
+ ASSERT_EQ(value[i++], 0x03); // ASE[0] Target Latency
+ ASSERT_EQ(value[i++], 0x02); // ASE[0] Target Phy
+ ASSERT_EQ(value[i++], 0x06); // ASE[0].CodecID Coding Format
+ ASSERT_EQ(value[i++], 0x03); // ASE[0].CodecID Company ID LSB
+ ASSERT_EQ(value[i++], 0x02); // ASE[0].CodecID Company ID MSB
+ ASSERT_EQ(value[i++], 0x05); // ASE[0].CodecID Codec ID LSB
+ ASSERT_EQ(value[i++], 0x04); // ASE[0].CodecID Codec ID MSB
+
+ // ASE[0].Codec Spec. Conf. Length - LC3 specific
+ ASSERT_EQ(value[i++], 8 + 8); // * 4*2 bytes for 4 LTV types and lengths + 8
+ // bytes for the values
+ ASSERT_EQ(value[i++], 0x02); // Sampling Freq. Length
+ ASSERT_EQ(value[i++], 0x01); // Sampling Freq. Type
+ ASSERT_EQ(value[i++], 0x10); // Sampling Freq. Value
+ ASSERT_EQ(value[i++], 0x02); // Frame Duration. Length
+ ASSERT_EQ(value[i++], 0x02); // Frame Duration. Type
+ ASSERT_EQ(value[i++], 0x03); // Frame Duration. Value
+ ASSERT_EQ(value[i++], 0x05); // Audio Channel Allocations Length
+ ASSERT_EQ(value[i++], 0x03); // Audio Channel Allocations Type
+ ASSERT_EQ(value[i++], 0x07); // Audio Channel Allocations Value[0]
+ ASSERT_EQ(value[i++], 0x06); // Audio Channel Allocations Value[1]
+ ASSERT_EQ(value[i++], 0x05); // Audio Channel Allocations Value[2]
+ ASSERT_EQ(value[i++], 0x04); // Audio Channel Allocations Value[3]
+ ASSERT_EQ(value[i++], 0x03); // Octets Per Frame Length
+ ASSERT_EQ(value[i++], 0x04); // Octets Per Frame Type
+ ASSERT_EQ(value[i++], 0x03); // Octets Per Frame Value[0]
+ ASSERT_EQ(value[i++], 0x02); // Octets Per Frame Value[1]
+ ASSERT_EQ(value.size(), i);
+
+ types::LeAudioCodecId codec_id2{.coding_format = 0x16,
+ .vendor_company_id = 0x1213,
+ .vendor_codec_id = 0x1415};
+ types::LeAudioLc3Config codec_conf2{.sampling_frequency = 0x11,
+ .frame_duration = 0x13,
+ .audio_channel_allocation = 0x14151617,
+ .octets_per_codec_frame = 0x1213};
+
+ confs.push_back(ctp_codec_conf{
+ .ase_id = 0x15,
+ .target_latency = 0x13,
+ .target_phy = 0x01,
+ .codec_id = codec_id2,
+ .codec_config = codec_conf2,
+ });
+ PrepareAseCtpCodecConfig(confs, value);
+
+ i = 0;
+ ASSERT_NE(value.size(), 0u);
+ ASSERT_EQ(value[i++], 0x01); // Config Codec Opcode
+ ASSERT_EQ(value[i++], 0x02); // Number of ASEs
+ ASSERT_EQ(value[i++], 0x05); // ASE[0] ASE ID
+ ASSERT_EQ(value[i++], 0x03); // ASE[0] Target Latency
+ ASSERT_EQ(value[i++], 0x02); // ASE[0] Target Phy
+ ASSERT_EQ(value[i++], 0x06); // ASE[0].CodecID Coding Format
+ ASSERT_EQ(value[i++], 0x03); // ASE[0].CodecID Company ID LSB
+ ASSERT_EQ(value[i++], 0x02); // ASE[0].CodecID Company ID MSB
+ ASSERT_EQ(value[i++], 0x05); // ASE[0].CodecID Codec ID LSB
+ ASSERT_EQ(value[i++], 0x04); // ASE[0].CodecID Codec ID MSB
+
+ // ASE[0].Codec Spec. Conf. Length - LC3 specific
+ ASSERT_EQ(value[i++], 8 + 8); // * 4*2 bytes for 4 LTV types and lengths + 8
+ // bytes for the values
+ ASSERT_EQ(value[i++], 0x02); // Sampling Freq. Length
+ ASSERT_EQ(value[i++], 0x01); // Sampling Freq. Type
+ ASSERT_EQ(value[i++], 0x10); // Sampling Freq. Value
+ ASSERT_EQ(value[i++], 0x02); // Frame Duration. Length
+ ASSERT_EQ(value[i++], 0x02); // Frame Duration. Type
+ ASSERT_EQ(value[i++], 0x03); // Frame Duration. Value
+ ASSERT_EQ(value[i++], 0x05); // Audio Channel Allocations Length
+ ASSERT_EQ(value[i++], 0x03); // Audio Channel Allocations Type
+ ASSERT_EQ(value[i++], 0x07); // Audio Channel Allocations Value[0]
+ ASSERT_EQ(value[i++], 0x06); // Audio Channel Allocations Value[1]
+ ASSERT_EQ(value[i++], 0x05); // Audio Channel Allocations Value[2]
+ ASSERT_EQ(value[i++], 0x04); // Audio Channel Allocations Value[3]
+ ASSERT_EQ(value[i++], 0x03); // Octets Per Frame Length
+ ASSERT_EQ(value[i++], 0x04); // Octets Per Frame Type
+ ASSERT_EQ(value[i++], 0x03); // Octets Per Frame Value[0]
+ ASSERT_EQ(value[i++], 0x02); // Octets Per Frame Value[1]
+
+ ASSERT_EQ(value[i++], 0x15); // ASE[1] ASE ID
+ ASSERT_EQ(value[i++], 0x13); // ASE[1] Target Latency
+ ASSERT_EQ(value[i++], 0x01); // ASE[1] Target Phy
+ ASSERT_EQ(value[i++], 0x16); // ASE[1].CodecID Coding Format
+ ASSERT_EQ(value[i++], 0x13); // ASE[1].CodecID Company ID LSB
+ ASSERT_EQ(value[i++], 0x12); // ASE[1].CodecID Company ID MSB
+ ASSERT_EQ(value[i++], 0x15); // ASE[1].CodecID Codec ID LSB
+ ASSERT_EQ(value[i++], 0x14); // ASE[1].CodecID Codec ID MSB
+
+ // ASE[1].Codec Spec. Conf. Length - LC3 specific
+ ASSERT_EQ(value[i++], 8 + 8); // * 4*2 bytes for 4 LTV types and lengths + 8
+ // bytes for the values
+ ASSERT_EQ(value[i++], 0x02); // Sampling Freq. Length
+ ASSERT_EQ(value[i++], 0x01); // Sampling Freq. Type
+ ASSERT_EQ(value[i++], 0x11); // Sampling Freq. Value
+ ASSERT_EQ(value[i++], 0x02); // Frame Duration. Length
+ ASSERT_EQ(value[i++], 0x02); // Frame Duration. Type
+ ASSERT_EQ(value[i++], 0x13); // Frame Duration. Value
+ ASSERT_EQ(value[i++], 0x05); // Audio Channel Allocations Length
+ ASSERT_EQ(value[i++], 0x03); // Audio Channel Allocations Type
+ ASSERT_EQ(value[i++], 0x17); // Audio Channel Allocations Value[0]
+ ASSERT_EQ(value[i++], 0x16); // Audio Channel Allocations Value[1]
+ ASSERT_EQ(value[i++], 0x15); // Audio Channel Allocations Value[2]
+ ASSERT_EQ(value[i++], 0x14); // Audio Channel Allocations Value[3]
+ ASSERT_EQ(value[i++], 0x03); // Octets Per Frame Length
+ ASSERT_EQ(value[i++], 0x04); // Octets Per Frame Type
+ ASSERT_EQ(value[i++], 0x13); // Octets Per Frame Value[0]
+ ASSERT_EQ(value[i++], 0x12); // Octets Per Frame Value[1]
+
+ ASSERT_EQ(value.size(), i);
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpConfigQosEmpty) {
+ std::vector<struct ctp_qos_conf> confs;
+ std::vector<uint8_t> value;
+
+ PrepareAseCtpConfigQos(confs, value);
+ ASSERT_EQ(value.size(), 0u);
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpConfigQosSingle) {
+ std::vector<struct ctp_qos_conf> confs;
+ std::vector<uint8_t> value;
+
+ const ctp_qos_conf conf{.ase_id = 0x01,
+ .cig = 0x11,
+ .cis = 0x12,
+ .sdu_interval = 0x00131415,
+ .framing = 0x01,
+ .phy = 0x01,
+ .max_sdu = 0x0203,
+ .retrans_nb = 0x04,
+ .max_transport_latency = 0x0302,
+ .pres_delay = 0x00121314};
+ confs.push_back(conf);
+
+ PrepareAseCtpConfigQos(confs, value);
+ ASSERT_NE(value.size(), 0u);
+
+ uint8_t i = 0;
+ ASSERT_EQ(value[i++], 0x02u); // Config QOS Opcode
+ ASSERT_EQ(value[i++], 0x01u); // Number of ASE
+ ASSERT_EQ(value[i++], 0x01u); // ASE ID
+ ASSERT_EQ(value[i++], 0x11u); // CIG ID
+ ASSERT_EQ(value[i++], 0x12u); // CIS ID
+ ASSERT_EQ(value[i++], 0x15u); // SDU Interval [0]
+ ASSERT_EQ(value[i++], 0x14u); // SDU Interval [1]
+ ASSERT_EQ(value[i++], 0x13u); // SDU Interval [2]
+ ASSERT_EQ(value[i++], 0x01u); // Framing
+ ASSERT_EQ(value[i++], 0x01u); // Phy
+ ASSERT_EQ(value[i++], 0x03u); // Max SDU LSB
+ ASSERT_EQ(value[i++], 0x02u); // Max SDU MSB
+ ASSERT_EQ(value[i++], 0x04u); // Retransmission
+ ASSERT_EQ(value[i++], 0x02u); // Max. Trans. Latency LSB
+ ASSERT_EQ(value[i++], 0x03u); // Max. Trans. Latency MSB
+ ASSERT_EQ(value[i++], 0x14u); // Pres. Delay[0]
+ ASSERT_EQ(value[i++], 0x13u); // Pres. Delay[1]
+ ASSERT_EQ(value[i++], 0x12u); // Pres. Delay[2]
+ ASSERT_EQ(value.size(), i);
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpConfigQosMultiple) {
+ std::vector<struct ctp_qos_conf> confs;
+ std::vector<uint8_t> value;
+
+ const ctp_qos_conf conf{.ase_id = 0x01,
+ .cig = 0x11,
+ .cis = 0x12,
+ .sdu_interval = 0x131415,
+ .framing = 0x01,
+ .phy = 0x01,
+ .max_sdu = 0x0203,
+ .retrans_nb = 0x04,
+ .max_transport_latency = 0x0302,
+ .pres_delay = 0x121314};
+ confs.push_back(conf);
+
+ const ctp_qos_conf conf2{.ase_id = 0x11,
+ .cig = 0x21,
+ .cis = 0x22,
+ .sdu_interval = 0x232425,
+ .framing = 0x02,
+ .phy = 0x02,
+ .max_sdu = 0x2223,
+ .retrans_nb = 0x24,
+ .max_transport_latency = 0x2322,
+ .pres_delay = 0x222324};
+ confs.push_back(conf2);
+
+ PrepareAseCtpConfigQos(confs, value);
+ ASSERT_NE(value.size(), 0u);
+
+ uint8_t i = 0;
+ ASSERT_EQ(value[i++], 0x02u); // Config QOS Opcode
+ ASSERT_EQ(value[i++], 0x02u); // Number of ASE
+ // 1st ASE Config
+ ASSERT_EQ(value[i++], 0x01u); // ASE ID
+ ASSERT_EQ(value[i++], 0x11u); // CIG ID
+ ASSERT_EQ(value[i++], 0x12u); // CIS ID
+ ASSERT_EQ(value[i++], 0x15u); // SDU Interval [0]
+ ASSERT_EQ(value[i++], 0x14u); // SDU Interval [1]
+ ASSERT_EQ(value[i++], 0x13u); // SDU Interval [2]
+ ASSERT_EQ(value[i++], 0x01u); // Framing
+ ASSERT_EQ(value[i++], 0x01u); // Phy
+ ASSERT_EQ(value[i++], 0x03u); // Max SDU LSB
+ ASSERT_EQ(value[i++], 0x02u); // Max SDU MSB
+ ASSERT_EQ(value[i++], 0x04u); // Retransmission
+ ASSERT_EQ(value[i++], 0x02u); // Max. Trans. Latency LSB
+ ASSERT_EQ(value[i++], 0x03u); // Max. Trans. Latency MSB
+ ASSERT_EQ(value[i++], 0x14u); // Pres. Delay[0]
+ ASSERT_EQ(value[i++], 0x13u); // Pres. Delay[1]
+ ASSERT_EQ(value[i++], 0x12u); // Pres. Delay[2]
+ // 2nd ASE Config
+ ASSERT_EQ(value[i++], 0x11u); // ASE ID
+ ASSERT_EQ(value[i++], 0x21u); // CIG ID
+ ASSERT_EQ(value[i++], 0x22u); // CIS ID
+ ASSERT_EQ(value[i++], 0x25u); // SDU Interval [0]
+ ASSERT_EQ(value[i++], 0x24u); // SDU Interval [1]
+ ASSERT_EQ(value[i++], 0x23u); // SDU Interval [2]
+ ASSERT_EQ(value[i++], 0x02u); // Framing
+ ASSERT_EQ(value[i++], 0x02u); // Phy
+ ASSERT_EQ(value[i++], 0x23u); // Max SDU LSB
+ ASSERT_EQ(value[i++], 0x22u); // Max SDU MSB
+ ASSERT_EQ(value[i++], 0x24u); // Retransmission
+ ASSERT_EQ(value[i++], 0x22u); // Max. Trans. Latency LSB
+ ASSERT_EQ(value[i++], 0x23u); // Max. Trans. Latency MSB
+ ASSERT_EQ(value[i++], 0x24u); // Pres. Delay[0]
+ ASSERT_EQ(value[i++], 0x23u); // Pres. Delay[1]
+ ASSERT_EQ(value[i++], 0x22u); // Pres. Delay[2]
+
+ ASSERT_EQ(value.size(), i);
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpEnableEmpty) {
+ std::vector<struct ctp_enable> confs;
+ std::vector<uint8_t> value;
+
+ PrepareAseCtpEnable(confs, value);
+ ASSERT_EQ(value.size(), 0u);
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpEnableSingle) {
+ std::vector<struct ctp_enable> confs;
+ std::vector<uint8_t> value;
+
+ ctp_enable conf{.ase_id = 0x11, .metadata = {0x02, 0x22, 0x21}};
+ confs.push_back(conf);
+
+ PrepareAseCtpEnable(confs, value);
+ ASSERT_NE(value.size(), 0u);
+
+ uint8_t i = 0;
+ ASSERT_EQ(value[i++], 0x03u); // Enable Opcode
+ ASSERT_EQ(value[i++], 0x01u); // Number of ASEs
+ ASSERT_EQ(value[i++], 0x11u); // ASE[0] ID
+ ASSERT_EQ(value[i++], 0x03u); // Metadata Len
+ ASSERT_EQ(value[i++], 0x02u); // Metadata[0]
+ ASSERT_EQ(value[i++], 0x22u); // Metadata[1]
+ ASSERT_EQ(value[i++], 0x21u); // Metadata[2]
+ ASSERT_EQ(value.size(), i);
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpEnableMultiple) {
+ std::vector<struct ctp_enable> confs;
+ std::vector<uint8_t> value;
+
+ ctp_enable conf{.ase_id = 0x11, .metadata = {0x02, 0x22, 0x21}};
+ confs.push_back(conf);
+
+ ctp_enable conf2{.ase_id = 0x21, .metadata = {0x03, 0x35, 0x36, 0x37}};
+ confs.push_back(conf2);
+
+ PrepareAseCtpEnable(confs, value);
+ ASSERT_NE(value.size(), 0u);
+
+ uint8_t i = 0;
+ ASSERT_EQ(value[i++], 0x03u); // Enable Opcode
+ ASSERT_EQ(value[i++], 0x02u); // Number of ASEs
+ ASSERT_EQ(value[i++], 0x11u); // ASE[0] ID
+ ASSERT_EQ(value[i++], 0x03u); // ASE[0] Metadata Len
+ ASSERT_EQ(value[i++], 0x02u); // ASE[0] Metadata[0]
+ ASSERT_EQ(value[i++], 0x22u); // ASE[0] Metadata[1]
+ ASSERT_EQ(value[i++], 0x21u); // ASE[0] Metadata[2]
+ ASSERT_EQ(value[i++], 0x21u); // ASE[1] ID
+ ASSERT_EQ(value[i++], 0x04u); // ASE[1] Metadata Len
+ ASSERT_EQ(value[i++], 0x03u); // ASE[1] Metadata[0]
+ ASSERT_EQ(value[i++], 0x35u); // ASE[1] Metadata[1]
+ ASSERT_EQ(value[i++], 0x36u); // ASE[1] Metadata[2]
+ ASSERT_EQ(value[i++], 0x37u); // ASE[1] Metadata[3]
+ ASSERT_EQ(value.size(), i);
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpAudioReceiverStartReadyEmpty) {
+ std::vector<uint8_t> ase_ids;
+ std::vector<uint8_t> value;
+
+ PrepareAseCtpAudioReceiverStartReady(ase_ids, value);
+ ASSERT_EQ(value.size(), 0u);
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpAudioReceiverStartReadySingle) {
+ std::vector<uint8_t> ase_ids;
+ std::vector<uint8_t> value;
+
+ ase_ids.push_back(0x11);
+
+ PrepareAseCtpAudioReceiverStartReady(ase_ids, value);
+
+ uint8_t i = 0;
+ ASSERT_NE(value.size(), 0u);
+ ASSERT_EQ(value[i++], 0x04u); // Receiver Start Ready Opcode
+ ASSERT_EQ(value[i++], 1u); // Number of ASEs
+ ASSERT_EQ(value[i++], 0x11u); // ASE[0] ID
+ ASSERT_EQ(i, value.size());
+}
+
+TEST(LeAudioClientParserTest,
+ testPrepareAseCtpAudioReceiverStartReadyMultiple) {
+ std::vector<uint8_t> ase_ids;
+ std::vector<uint8_t> value;
+
+ ase_ids.push_back(0x11);
+ ase_ids.push_back(0x36);
+
+ PrepareAseCtpAudioReceiverStartReady(ase_ids, value);
+
+ uint8_t i = 0;
+ ASSERT_NE(value.size(), 0u);
+ ASSERT_EQ(value[i++], 0x04u); // Receiver Start Ready Opcode
+ ASSERT_EQ(value[i++], 2u); // Number of ASEs
+ ASSERT_EQ(value[i++], 0x11u); // ASE[0] ID
+ ASSERT_EQ(value[i++], 0x36u); // ASE[0] ID
+ ASSERT_EQ(i, value.size());
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpDisableEmpty) {
+ std::vector<uint8_t> ase_ids;
+ std::vector<uint8_t> value;
+
+ PrepareAseCtpDisable(ase_ids, value);
+ ASSERT_EQ(value.size(), 0u);
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpDisableSingle) {
+ std::vector<uint8_t> ase_ids;
+ std::vector<uint8_t> value;
+
+ ase_ids.push_back(0x11);
+
+ PrepareAseCtpDisable(ase_ids, value);
+
+ uint8_t i = 0;
+ ASSERT_NE(value.size(), 0u);
+ ASSERT_EQ(value[i++], 0x05u); // Disable Opcode
+ ASSERT_EQ(value[i++], 1u); // Number of ASEs
+ ASSERT_EQ(value[i++], 0x11u); // ASE[0] ID
+ ASSERT_EQ(i, value.size());
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpDisableMultiple) {
+ std::vector<uint8_t> ase_ids;
+ std::vector<uint8_t> value;
+
+ ase_ids.push_back(0x11);
+ ase_ids.push_back(0x36);
+
+ PrepareAseCtpDisable(ase_ids, value);
+
+ uint8_t i = 0;
+ ASSERT_NE(value.size(), 0u);
+ ASSERT_EQ(value[i++], 0x05u); // Disable Opcode
+ ASSERT_EQ(value[i++], 2u); // Number of ASEs
+ ASSERT_EQ(value[i++], 0x11u); // ASE[0] ID
+ ASSERT_EQ(value[i++], 0x36u); // ASE[0] ID
+ ASSERT_EQ(i, value.size());
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpAudioReceiverStopReadyEmpty) {
+ std::vector<uint8_t> ase_ids;
+ std::vector<uint8_t> value;
+
+ PrepareAseCtpAudioReceiverStopReady(ase_ids, value);
+ ASSERT_EQ(value.size(), 0u);
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpAudioReceiverStopReadySingle) {
+ std::vector<uint8_t> ase_ids;
+ std::vector<uint8_t> value;
+
+ ase_ids.push_back(0x11);
+
+ PrepareAseCtpAudioReceiverStopReady(ase_ids, value);
+
+ uint8_t i = 0;
+ ASSERT_NE(value.size(), 0u);
+ ASSERT_EQ(value[i++], 0x06u); // Reveicer Stop Ready Opcode
+ ASSERT_EQ(value[i++], 1u); // Number of ASEs
+ ASSERT_EQ(value[i++], 0x11u); // ASE[0] ID
+ ASSERT_EQ(i, value.size());
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpAudioReceiverStopReadyMultiple) {
+ std::vector<uint8_t> ase_ids;
+ std::vector<uint8_t> value;
+
+ ase_ids.push_back(0x11);
+ ase_ids.push_back(0x36);
+
+ PrepareAseCtpAudioReceiverStopReady(ase_ids, value);
+
+ uint8_t i = 0;
+ ASSERT_NE(value.size(), 0u);
+ ASSERT_EQ(value[i++], 0x06u); // Reveicer Stop Ready Opcode
+ ASSERT_EQ(value[i++], 2u); // Number of ASEs
+ ASSERT_EQ(value[i++], 0x11u); // ASE[0] ID
+ ASSERT_EQ(value[i++], 0x36u); // ASE[0] ID
+ ASSERT_EQ(i, value.size());
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpUpdateMetadataEmpty) {
+ std::vector<struct ctp_update_metadata> confs;
+ std::vector<uint8_t> value;
+
+ PrepareAseCtpUpdateMetadata(confs, value);
+ ASSERT_EQ(value.size(), 0u);
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpUpdateMetadataSingle) {
+ std::vector<struct ctp_update_metadata> confs;
+ std::vector<uint8_t> value;
+
+ ctp_update_metadata conf{.ase_id = 0x11, .metadata = {0x02, 0x22, 0x21}};
+ confs.push_back(conf);
+
+ PrepareAseCtpUpdateMetadata(confs, value);
+ ASSERT_NE(value.size(), 0u);
+
+ uint8_t i = 0;
+ ASSERT_EQ(value[i++], 0x07u); // Update Metadata Opcode
+ ASSERT_EQ(value[i++], 0x01u); // Number of ASEs
+ ASSERT_EQ(value[i++], 0x11u); // ASE[0] ID
+ ASSERT_EQ(value[i++], 0x03u); // Metadata Len
+ ASSERT_EQ(value[i++], 0x02u); // Metadata[0]
+ ASSERT_EQ(value[i++], 0x22u); // Metadata[1]
+ ASSERT_EQ(value[i++], 0x21u); // Metadata[2]
+ ASSERT_EQ(i, value.size());
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpUpdateMetadataMultiple) {
+ std::vector<struct ctp_update_metadata> confs;
+ std::vector<uint8_t> value;
+
+ ctp_update_metadata conf{.ase_id = 0x11, .metadata = {0x02, 0x22, 0x21}};
+ confs.push_back(conf);
+
+ ctp_update_metadata conf2{.ase_id = 0x21,
+ .metadata = {0x03, 0x35, 0x36, 0x37}};
+ confs.push_back(conf2);
+
+ PrepareAseCtpUpdateMetadata(confs, value);
+ ASSERT_NE(value.size(), 0u);
+
+ uint8_t i = 0;
+ ASSERT_EQ(value[i++], 0x07u); // Update Metadata Opcode
+ ASSERT_EQ(value[i++], 0x02u); // Number of ASEs
+ ASSERT_EQ(value[i++], 0x11u); // ASE[0] ID
+ ASSERT_EQ(value[i++], 0x03u); // ASE[0] Metadata Len
+ ASSERT_EQ(value[i++], 0x02u); // ASE[0] Metadata[0]
+ ASSERT_EQ(value[i++], 0x22u); // ASE[0] Metadata[1]
+ ASSERT_EQ(value[i++], 0x21u); // ASE[0] Metadata[2]
+ ASSERT_EQ(value[i++], 0x21u); // ASE[1] ID
+ ASSERT_EQ(value[i++], 0x04u); // ASE[1] Metadata Len
+ ASSERT_EQ(value[i++], 0x03u); // ASE[1] Metadata[0]
+ ASSERT_EQ(value[i++], 0x35u); // ASE[1] Metadata[1]
+ ASSERT_EQ(value[i++], 0x36u); // ASE[1] Metadata[2]
+ ASSERT_EQ(value[i++], 0x37u); // ASE[1] Metadata[2]
+ ASSERT_EQ(i, value.size());
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpReleaseEmpty) {
+ std::vector<uint8_t> ase_ids;
+ std::vector<uint8_t> value;
+
+ PrepareAseCtpRelease(ase_ids, value);
+ ASSERT_EQ(value.size(), 0u);
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpReleaseSingle) {
+ std::vector<uint8_t> ase_ids;
+ std::vector<uint8_t> value;
+
+ ase_ids.push_back(0x11);
+
+ PrepareAseCtpRelease(ase_ids, value);
+
+ uint8_t i = 0;
+ ASSERT_NE(value.size(), 0u);
+ ASSERT_EQ(value[i++], 0x08u); // Release Opcode
+ ASSERT_EQ(value[i++], 1u); // Number of ASEs
+ ASSERT_EQ(value[i++], 0x11u); // ASE[0] ID
+ ASSERT_EQ(i, value.size());
+}
+
+TEST(LeAudioClientParserTest, testPrepareAseCtpReleaseMultiple) {
+ std::vector<uint8_t> ase_ids;
+ std::vector<uint8_t> value;
+
+ ase_ids.push_back(0x11);
+ ase_ids.push_back(0x36);
+
+ PrepareAseCtpRelease(ase_ids, value);
+
+ uint8_t i = 0;
+ ASSERT_NE(value.size(), 0u);
+ ASSERT_EQ(value[i++], 0x08u); // Release Opcode
+ ASSERT_EQ(value[i++], 2u); // Number of ASEs
+ ASSERT_EQ(value[i++], 0x11u); // ASE[0] ID
+ ASSERT_EQ(value[i++], 0x36u); // ASE[0] ID
+ ASSERT_EQ(i, value.size());
+}
+
+} // namespace ascs
+
+} // namespace client_parser
+} // namespace le_audio
diff --git a/bta/le_audio/devices.cc b/bta/le_audio/devices.cc
new file mode 100644
index 000000000..1fd6f6170
--- /dev/null
+++ b/bta/le_audio/devices.cc
@@ -0,0 +1,1778 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com. Represented by EHIMA
+ * - www.ehima.com
+ *
+ * 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.
+ */
+
+#include "devices.h"
+
+#include <base/strings/string_number_conversions.h>
+
+#include <map>
+
+#include "bta_gatt_queue.h"
+#include "bta_groups.h"
+#include "bta_le_audio_api.h"
+#include "btm_iso_api.h"
+#include "btm_iso_api_types.h"
+#include "client_audio.h"
+#include "device/include/controller.h"
+#include "stack/btm/btm_int.h"
+
+using bluetooth::hci::kIsoCigFramingFramed;
+using bluetooth::hci::kIsoCigFramingUnframed;
+using bluetooth::hci::kIsoCigPackingSequential;
+using bluetooth::hci::kIsoCigPhy1M;
+using bluetooth::hci::kIsoCigPhy2M;
+using bluetooth::hci::iso_manager::kIsoSca0To20Ppm;
+using le_audio::set_configurations::CodecCapabilitySetting;
+using le_audio::types::ase;
+using le_audio::types::AseState;
+using le_audio::types::AudioContexts;
+using le_audio::types::AudioLocations;
+using le_audio::types::AudioStreamDataPathState;
+using le_audio::types::BidirectAsesPair;
+using le_audio::types::LeAudioCodecId;
+using le_audio::types::LeAudioContextType;
+using le_audio::types::LeAudioLc3Config;
+
+namespace le_audio {
+/* LeAudioDeviceGroup Class methods implementation */
+void LeAudioDeviceGroup::AddNode(
+ const std::shared_ptr<LeAudioDevice>& leAudioDevice) {
+ leAudioDevice->group_id_ = group_id_;
+ leAudioDevices_.push_back(std::weak_ptr<LeAudioDevice>(leAudioDevice));
+}
+
+void LeAudioDeviceGroup::RemoveNode(
+ const std::shared_ptr<LeAudioDevice>& leAudioDevice) {
+ leAudioDevice->group_id_ = bluetooth::groups::kGroupUnknown;
+ leAudioDevices_.erase(
+ std::remove_if(
+ leAudioDevices_.begin(), leAudioDevices_.end(),
+ [&leAudioDevice](auto& d) { return d.lock() == leAudioDevice; }),
+ leAudioDevices_.end());
+}
+
+bool LeAudioDeviceGroup::IsEmpty(void) { return leAudioDevices_.size() == 0; }
+
+bool LeAudioDeviceGroup::IsAnyDeviceConnected(void) {
+ return (NumOfConnected() != 0);
+}
+
+int LeAudioDeviceGroup::Size(void) { return leAudioDevices_.size(); }
+
+int LeAudioDeviceGroup::NumOfConnected(types::LeAudioContextType context_type) {
+ if (leAudioDevices_.empty()) return 0;
+
+ bool check_context_type = (context_type != LeAudioContextType::RFU);
+ AudioContexts type_set = static_cast<uint16_t>(context_type);
+
+ /* return number of connected devices from the set*/
+ return std::count_if(
+ leAudioDevices_.begin(), leAudioDevices_.end(),
+ [type_set, check_context_type](auto& iter) {
+ if (iter.expired()) return false;
+ if (iter.lock()->conn_id_ == GATT_INVALID_CONN_ID) return false;
+
+ if (!check_context_type) return true;
+
+ return (iter.lock()->GetAvailableContexts() & type_set).any();
+ });
+}
+
+void LeAudioDeviceGroup::Cleanup(void) { leAudioDevices_.clear(); }
+
+void LeAudioDeviceGroup::Deactivate(void) {
+ for (auto* leAudioDevice = GetFirstActiveDevice(); leAudioDevice;
+ leAudioDevice = GetNextActiveDevice(leAudioDevice)) {
+ for (auto* ase = leAudioDevice->GetFirstActiveAse(); ase;
+ ase = leAudioDevice->GetNextActiveAse(ase)) {
+ ase->active = false;
+ }
+ }
+}
+
+LeAudioDevice* LeAudioDeviceGroup::GetFirstDevice(void) {
+ return (leAudioDevices_.front().lock()).get();
+}
+
+LeAudioDevice* LeAudioDeviceGroup::GetFirstDeviceWithActiveContext(
+ types::LeAudioContextType context_type) {
+ AudioContexts type_set = static_cast<uint16_t>(context_type);
+
+ auto iter = std::find_if(
+ leAudioDevices_.begin(), leAudioDevices_.end(), [&type_set](auto& iter) {
+ if (iter.expired()) return false;
+ return (iter.lock()->GetAvailableContexts() & type_set).any();
+ });
+
+ if ((iter == leAudioDevices_.end()) || (iter->expired())) return nullptr;
+
+ return (iter->lock()).get();
+}
+
+LeAudioDevice* LeAudioDeviceGroup::GetNextDevice(LeAudioDevice* leAudioDevice) {
+ auto iter = std::find_if(leAudioDevices_.begin(), leAudioDevices_.end(),
+ [&leAudioDevice](auto& d) {
+ if (d.expired())
+ return false;
+ else
+ return (d.lock()).get() == leAudioDevice;
+ });
+
+ /* If reference device not found */
+ if (iter == leAudioDevices_.end()) return nullptr;
+
+ std::advance(iter, 1);
+ /* If reference device is last in group */
+ if (iter == leAudioDevices_.end()) return nullptr;
+
+ if (iter->expired()) return nullptr;
+
+ return (iter->lock()).get();
+}
+
+LeAudioDevice* LeAudioDeviceGroup::GetNextDeviceWithActiveContext(
+ LeAudioDevice* leAudioDevice, types::LeAudioContextType context_type) {
+ AudioContexts type_set = static_cast<uint16_t>(context_type);
+
+ auto iter = std::find_if(leAudioDevices_.begin(), leAudioDevices_.end(),
+ [&leAudioDevice](auto& d) {
+ if (d.expired())
+ return false;
+ else
+ return (d.lock()).get() == leAudioDevice;
+ });
+
+ /* If reference device not found */
+ if (iter == leAudioDevices_.end()) return nullptr;
+
+ std::advance(iter, 1);
+ /* If reference device is last in group */
+ if (iter == leAudioDevices_.end()) return nullptr;
+
+ iter = std::find_if(iter, leAudioDevices_.end(), [&type_set](auto& d) {
+ if (d.expired())
+ return false;
+ else
+ return (d.lock()->GetAvailableContexts() & type_set).any();
+ ;
+ });
+
+ return (iter == leAudioDevices_.end()) ? nullptr : (iter->lock()).get();
+}
+
+bool LeAudioDeviceGroup::IsDeviceInTheGroup(LeAudioDevice* leAudioDevice) {
+ auto iter = std::find_if(leAudioDevices_.begin(), leAudioDevices_.end(),
+ [&leAudioDevice](auto& d) {
+ if (d.expired())
+ return false;
+ else
+ return (d.lock()).get() == leAudioDevice;
+ });
+
+ if ((iter == leAudioDevices_.end()) || (iter->expired())) return false;
+
+ return true;
+}
+
+bool LeAudioDeviceGroup::HaveAllActiveDevicesAsesTheSameState(AseState state) {
+ auto iter = std::find_if(
+ leAudioDevices_.begin(), leAudioDevices_.end(), [&state](auto& d) {
+ if (d.expired())
+ return false;
+ else
+ return !(((d.lock()).get())->HaveAllActiveAsesSameState(state));
+ });
+
+ return iter == leAudioDevices_.end();
+}
+
+LeAudioDevice* LeAudioDeviceGroup::GetFirstActiveDevice(void) {
+ auto iter =
+ std::find_if(leAudioDevices_.begin(), leAudioDevices_.end(), [](auto& d) {
+ if (d.expired())
+ return false;
+ else
+ return ((d.lock()).get())->HaveActiveAse();
+ });
+
+ if (iter == leAudioDevices_.end() || iter->expired()) return nullptr;
+
+ return (iter->lock()).get();
+}
+
+LeAudioDevice* LeAudioDeviceGroup::GetNextActiveDevice(
+ LeAudioDevice* leAudioDevice) {
+ auto iter = std::find_if(leAudioDevices_.begin(), leAudioDevices_.end(),
+ [&leAudioDevice](auto& d) {
+ if (d.expired())
+ return false;
+ else
+ return (d.lock()).get() == leAudioDevice;
+ });
+
+ if (iter == leAudioDevices_.end() ||
+ std::distance(iter, leAudioDevices_.end()) < 1)
+ return nullptr;
+
+ iter = std::find_if(std::next(iter, 1), leAudioDevices_.end(), [](auto& d) {
+ if (d.expired())
+ return false;
+ else
+ return ((d.lock()).get())->HaveActiveAse();
+ });
+
+ return (iter == leAudioDevices_.end()) ? nullptr : (iter->lock()).get();
+}
+
+bool LeAudioDeviceGroup::SetContextType(LeAudioContextType context_type) {
+ /* XXX: group context policy ? / may it disallow to change type ?) */
+ context_type_ = context_type;
+
+ return true;
+}
+
+LeAudioContextType LeAudioDeviceGroup::GetContextType(void) {
+ return context_type_;
+}
+
+uint32_t LeAudioDeviceGroup::GetSduInterval(uint8_t direction) {
+ for (LeAudioDevice* leAudioDevice = GetFirstActiveDevice();
+ leAudioDevice != nullptr;
+ leAudioDevice = GetNextActiveDevice(leAudioDevice)) {
+ struct ase* ase = leAudioDevice->GetFirstActiveAseByDirection(direction);
+ if (!ase) continue;
+
+ return ase->codec_config.GetFrameDurationUs();
+ }
+
+ return 0;
+}
+
+uint8_t LeAudioDeviceGroup::GetSCA(void) {
+ uint8_t sca = kIsoSca0To20Ppm;
+
+ for (const auto& leAudioDevice : leAudioDevices_) {
+ uint8_t dev_sca =
+ BTM_GetPeerSCA(leAudioDevice.lock()->address_, BT_TRANSPORT_LE);
+
+ /* If we could not read SCA from the peer device or sca is 0,
+ * then there is no reason to continue.
+ */
+ if ((dev_sca == 0xFF) || (dev_sca == 0)) return 0;
+
+ /* The Slaves_Clock_Accuracy parameter shall be the worst-case sleep clock
+ *accuracy of all the slaves that will participate in the CIG.
+ */
+ if (dev_sca < sca) {
+ sca = dev_sca;
+ }
+ }
+
+ return sca;
+}
+
+uint8_t LeAudioDeviceGroup::GetPacking(void) {
+ /* TODO: Decide about packing */
+ return kIsoCigPackingSequential;
+}
+
+uint8_t LeAudioDeviceGroup::GetFraming(void) {
+ LeAudioDevice* leAudioDevice = GetFirstActiveDevice();
+ LOG_ASSERT(leAudioDevice)
+ << __func__ << " Shouldn't be called without an active device.";
+
+ do {
+ struct ase* ase = leAudioDevice->GetFirstActiveAse();
+ if (!ase) continue;
+
+ do {
+ if (ase->framing == types::kFramingUnframedPduUnsupported)
+ return kIsoCigFramingFramed;
+ } while ((ase = leAudioDevice->GetNextActiveAse(ase)));
+ } while ((leAudioDevice = GetNextActiveDevice(leAudioDevice)));
+
+ return kIsoCigFramingUnframed;
+}
+
+uint8_t LeAudioDeviceGroup::GetTargetLatency(void) {
+ /* TODO: Decide about target latency */
+ return types::kTargetLatencyBalancedLatencyReliability;
+}
+
+/* TODO: Preferred parameter may be other than minimum */
+static uint16_t find_max_transport_latency(LeAudioDeviceGroup* group,
+ uint8_t direction) {
+ uint16_t max_transport_latency = 0;
+
+ for (LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
+ leAudioDevice != nullptr;
+ leAudioDevice = group->GetNextActiveDevice(leAudioDevice)) {
+ for (ase* ase = leAudioDevice->GetFirstActiveAseByDirection(direction);
+ ase != nullptr;
+ ase = leAudioDevice->GetNextActiveAseWithSameDirection(ase)) {
+ if (!ase) break;
+
+ if (!max_transport_latency)
+ // first assignment
+ max_transport_latency = ase->max_transport_latency;
+ else if (ase->max_transport_latency < max_transport_latency)
+ max_transport_latency = ase->max_transport_latency;
+ }
+ }
+
+ if (max_transport_latency < types::kMaxTransportLatencyMin)
+ max_transport_latency = types::kMaxTransportLatencyMin;
+ else if (max_transport_latency > types::kMaxTransportLatencyMax)
+ max_transport_latency = types::kMaxTransportLatencyMax;
+
+ return max_transport_latency;
+}
+
+uint16_t LeAudioDeviceGroup::GetMaxTransportLatencyStom(void) {
+ return find_max_transport_latency(this, types::kLeAudioDirectionSource);
+}
+
+uint16_t LeAudioDeviceGroup::GetMaxTransportLatencyMtos(void) {
+ return find_max_transport_latency(this, types::kLeAudioDirectionSink);
+}
+
+uint16_t LeAudioDeviceGroup::GetTransportLatency(uint8_t direction) {
+ if (direction == types::kLeAudioDirectionSink) {
+ return transport_latency_mtos_;
+ } else if (direction == types::kLeAudioDirectionSource) {
+ return transport_latency_stom_;
+ } else {
+ LOG(ERROR) << __func__ << ", invalid direction";
+ return 0;
+ }
+}
+
+void LeAudioDeviceGroup::SetTransportLatency(uint8_t direction,
+ uint16_t new_transport_latency) {
+ uint16_t* transport_latency;
+
+ if (direction == types::kLeAudioDirectionSink) {
+ transport_latency = &transport_latency_mtos_;
+ } else if (direction == types::kLeAudioDirectionSource) {
+ transport_latency = &transport_latency_stom_;
+ } else {
+ LOG(ERROR) << __func__ << ", invalid direction";
+ return;
+ }
+
+ if (*transport_latency == new_transport_latency) return;
+
+ if ((*transport_latency != 0) &&
+ (*transport_latency != new_transport_latency)) {
+ LOG(WARNING) << __func__ << ", Different transport latency for group: "
+ << " old: " << static_cast<int>(*transport_latency)
+ << " [ms], new: " << static_cast<int>(new_transport_latency)
+ << " [ms]";
+ return;
+ }
+
+ LOG(INFO) << __func__ << ", updated group " << static_cast<int>(group_id_)
+ << " transport latency: " << static_cast<int>(new_transport_latency)
+ << " [ms]";
+ *transport_latency = new_transport_latency;
+}
+
+uint8_t LeAudioDeviceGroup::GetPhyBitmask(uint8_t direction) {
+ LeAudioDevice* leAudioDevice = GetFirstActiveDevice();
+ LOG_ASSERT(leAudioDevice)
+ << __func__ << " Shouldn't be called without an active device.";
+
+ // local supported PHY's
+ uint8_t phy_bitfield = kIsoCigPhy1M;
+ if (controller_get_interface()->supports_ble_2m_phy())
+ phy_bitfield |= kIsoCigPhy2M;
+
+ if (!leAudioDevice) {
+ LOG(ERROR) << "No active leaudio device for direction?: " << +direction;
+ return phy_bitfield;
+ }
+
+ do {
+ struct ase* ase = leAudioDevice->GetFirstActiveAseByDirection(direction);
+ if (!ase) return phy_bitfield;
+
+ do {
+ if (direction == ase->direction) {
+ phy_bitfield &= leAudioDevice->GetPhyBitmask();
+
+ // A value of 0x00 denotes no preference
+ if (ase->preferred_phy) phy_bitfield &= ase->preferred_phy;
+ }
+ } while ((ase = leAudioDevice->GetNextActiveAseWithSameDirection(ase)));
+ } while ((leAudioDevice = GetNextActiveDevice(leAudioDevice)));
+
+ return phy_bitfield;
+}
+
+uint8_t LeAudioDeviceGroup::GetTargetPhy(uint8_t direction) {
+ uint8_t phy_bitfield = GetPhyBitmask(direction);
+
+ // prefer to use 2M if supported
+ if (phy_bitfield & kIsoCigPhy2M)
+ return types::kTargetPhy2M;
+ else if (phy_bitfield & kIsoCigPhy1M)
+ return types::kTargetPhy1M;
+ else
+ return 0;
+}
+
+bool LeAudioDeviceGroup::GetPresentationDelay(uint32_t* delay,
+ uint8_t direction) {
+ uint32_t delay_min = 0;
+ uint32_t delay_max = UINT32_MAX;
+ uint32_t preferred_delay_min = delay_min;
+ uint32_t preferred_delay_max = delay_max;
+
+ LeAudioDevice* leAudioDevice = GetFirstActiveDevice();
+ LOG_ASSERT(leAudioDevice)
+ << __func__ << " Shouldn't be called without an active device.";
+
+ do {
+ struct ase* ase = leAudioDevice->GetFirstActiveAseByDirection(direction);
+ if (!ase) continue; // device has no active ASEs in this direction
+
+ do {
+ /* No common range check */
+ if (ase->pres_delay_min > delay_max || ase->pres_delay_max < delay_min)
+ return false;
+
+ if (ase->pres_delay_min > delay_min) delay_min = ase->pres_delay_min;
+ if (ase->pres_delay_max < delay_max) delay_max = ase->pres_delay_max;
+ if (ase->preferred_pres_delay_min > preferred_delay_min)
+ preferred_delay_min = ase->preferred_pres_delay_min;
+ if (ase->preferred_pres_delay_max < preferred_delay_max &&
+ ase->preferred_pres_delay_max != types::kPresDelayNoPreference)
+ preferred_delay_max = ase->preferred_pres_delay_max;
+ } while ((ase = leAudioDevice->GetNextActiveAseWithSameDirection(ase)));
+ } while ((leAudioDevice = GetNextActiveDevice(leAudioDevice)));
+
+ if (preferred_delay_min <= preferred_delay_max &&
+ preferred_delay_min > delay_min && preferred_delay_min < delay_max) {
+ *delay = preferred_delay_min;
+ } else {
+ *delay = delay_min;
+ }
+
+ return true;
+}
+
+uint16_t LeAudioDeviceGroup::GetRemoteDelay(uint8_t direction) {
+ uint16_t remote_delay_ms = 0;
+ uint32_t presentation_delay;
+
+ if (!GetPresentationDelay(&presentation_delay, direction)) {
+ /* This should never happens at stream request time but to be safe return
+ * some sample value to not break streaming
+ */
+ return 100;
+ }
+
+ /* us to ms */
+ remote_delay_ms = presentation_delay / 1000;
+ remote_delay_ms += GetTransportLatency(direction) / 1000;
+
+ return remote_delay_ms;
+}
+
+/* This method returns AudioContext value if support for any type has changed */
+std::optional<AudioContexts> LeAudioDeviceGroup::UpdateActiveContextsMap(void) {
+ DLOG(INFO) << __func__ << " group id: " << group_id_ << " active contexts: "
+ << loghex(active_contexts_mask_.to_ulong());
+ return UpdateActiveContextsMap(active_contexts_mask_);
+}
+
+/* This method returns AudioContext value if support for any type has changed */
+std::optional<AudioContexts> LeAudioDeviceGroup::UpdateActiveContextsMap(
+ AudioContexts update_contexts) {
+ AudioContexts contexts = 0x0000;
+ bool active_contexts_has_been_modified = false;
+
+ for (LeAudioContextType ctx_type : types::kLeAudioContextAllTypesArray) {
+ AudioContexts type_set = static_cast<uint16_t>(ctx_type);
+
+ if ((type_set & update_contexts).none()) {
+ /* Fill context bitset for possible returned value if updated */
+ if (active_context_to_configuration_map.count(ctx_type) > 0)
+ contexts |= type_set;
+
+ continue;
+ }
+
+ auto new_conf = FindFirstSupportedConfiguration(ctx_type);
+
+ /* Check if support for context type has changed */
+ if (active_context_to_configuration_map.count(ctx_type) == 0 ||
+ active_context_to_configuration_map[ctx_type] == nullptr) {
+ /* Current configuration for context type is empty */
+ if (new_conf == nullptr) {
+ /* Configuration remains empty */
+ continue;
+ } else {
+ /* Configuration changes from empty to some */
+ contexts |= type_set;
+ active_contexts_has_been_modified = true;
+ }
+ } else {
+ /* Current configuration for context type is not empty */
+ if (new_conf == nullptr) {
+ /* Configuration changed to empty */
+ contexts &= ~type_set;
+ active_contexts_has_been_modified = true;
+ } else if (new_conf != active_context_to_configuration_map[ctx_type]) {
+ /* Configuration changed to any other */
+ contexts |= type_set;
+ active_contexts_has_been_modified = true;
+ } else {
+ /* Configuration is the same */
+ continue;
+ }
+ }
+
+ LOG(INFO) << __func__ << ", updated context: " << loghex(int(ctx_type))
+ << ", "
+ << (active_context_to_configuration_map[ctx_type] != nullptr
+ ? active_context_to_configuration_map[ctx_type]->name
+ : "empty")
+ << " -> " << (new_conf != nullptr ? new_conf->name : "empty");
+ active_context_to_configuration_map[ctx_type] = new_conf;
+ }
+
+ /* Some contexts have changed, return new active context bitset */
+ if (active_contexts_has_been_modified) {
+ active_contexts_mask_ = contexts;
+ return contexts;
+ }
+
+ /* Nothing has changed */
+ return std::nullopt;
+}
+
+bool LeAudioDeviceGroup::ReloadAudioLocations(void) {
+ AudioLocations updated_snk_audio_locations_ =
+ codec_spec_conf::kLeAudioLocationMonoUnspecified;
+ AudioLocations updated_src_audio_locations_ =
+ codec_spec_conf::kLeAudioLocationMonoUnspecified;
+
+ for (const auto& device : leAudioDevices_) {
+ if (device.expired()) continue;
+ updated_snk_audio_locations_ |= device.lock().get()->snk_audio_locations_;
+ updated_src_audio_locations_ |= device.lock().get()->src_audio_locations_;
+ }
+
+ /* Nothing has changed */
+ if ((updated_snk_audio_locations_ == snk_audio_locations_) &&
+ (updated_src_audio_locations_ == src_audio_locations_))
+ return false;
+
+ snk_audio_locations_ = updated_snk_audio_locations_;
+ src_audio_locations_ = updated_src_audio_locations_;
+
+ return true;
+}
+
+bool LeAudioDeviceGroup::IsInTransition(void) {
+ return target_state_ != current_state_;
+}
+
+bool LeAudioDeviceGroup::IsReleasing(void) {
+ return target_state_ == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE;
+}
+
+bool LeAudioDeviceGroup::IsGroupStreamReady(void) {
+ auto iter =
+ std::find_if(leAudioDevices_.begin(), leAudioDevices_.end(), [](auto& d) {
+ if (d.expired())
+ return false;
+ else
+ return !(((d.lock()).get())->HaveAllActiveAsesCisEst());
+ });
+
+ return iter == leAudioDevices_.end();
+}
+
+bool LeAudioDeviceGroup::HaveAllActiveDevicesCisDisc(void) {
+ auto iter =
+ std::find_if(leAudioDevices_.begin(), leAudioDevices_.end(), [](auto& d) {
+ if (d.expired())
+ return false;
+ else
+ return !(((d.lock()).get())->HaveAllAsesCisDisc());
+ });
+
+ return iter == leAudioDevices_.end();
+}
+
+uint8_t LeAudioDeviceGroup::GetFirstFreeCisId(void) {
+ for (uint8_t id = 0; id < UINT8_MAX; id++) {
+ auto iter = std::find_if(leAudioDevices_.begin(), leAudioDevices_.end(),
+ [id](auto& d) {
+ if (d.expired())
+ return false;
+ else
+ return ((d.lock()).get())->HasCisId(id);
+ });
+
+ if (iter == leAudioDevices_.end()) return id;
+ }
+
+ return kInvalidCisId;
+}
+
+bool CheckIfStrategySupported(types::LeAudioConfigurationStrategy strategy,
+ types::AudioLocations audio_locations,
+ uint8_t requested_channel_count,
+ uint8_t channel_count_mask) {
+ DLOG(INFO) << __func__ << " strategy: " << (int)strategy
+ << " locations: " << +audio_locations.to_ulong();
+
+ switch (strategy) {
+ case types::LeAudioConfigurationStrategy::MONO_ONE_CIS_PER_DEVICE:
+ return audio_locations.any();
+ case types::LeAudioConfigurationStrategy::STEREO_TWO_CISES_PER_DEVICE:
+ if ((audio_locations.to_ulong() &
+ codec_spec_conf::kLeAudioLocationAnyLeft) &&
+ (audio_locations.to_ulong() &
+ codec_spec_conf::kLeAudioLocationAnyRight))
+ return true;
+ else
+ return false;
+ case types::LeAudioConfigurationStrategy::STEREO_ONE_CIS_PER_DEVICE:
+ if (!(audio_locations.to_ulong() &
+ codec_spec_conf::kLeAudioLocationAnyLeft) ||
+ !(audio_locations.to_ulong() &
+ codec_spec_conf::kLeAudioLocationAnyRight))
+ return false;
+
+ DLOG(INFO) << __func__ << " requested chan cnt "
+ << +requested_channel_count
+ << " chan mask: " << loghex(channel_count_mask);
+
+ /* Return true if requested channel count is set in the channel count
+ * mask. In the channel_count_mask, bit0 is set when 1 channel is
+ * supported.
+ */
+ return ((1 << (requested_channel_count - 1)) & channel_count_mask);
+ default:
+ return false;
+ }
+
+ return false;
+}
+
+/* This method check if group support given audio configuration
+ * requirement for connected devices in the group and available ASEs
+ * (no matter on the ASE state) and for given context type
+ */
+bool LeAudioDeviceGroup::IsConfigurationSupported(
+ const set_configurations::AudioSetConfiguration* audio_set_conf,
+ types::LeAudioContextType context_type) {
+ if (!set_configurations::check_if_may_cover_scenario(
+ audio_set_conf, NumOfConnected(context_type))) {
+ DLOG(INFO) << __func__ << " cannot cover scenario "
+ << static_cast<int>(context_type)
+ << " size of for context type: "
+ << +NumOfConnected(context_type);
+ return false;
+ }
+
+ /* TODO For now: set ase if matching with first pac.
+ * 1) We assume as well that devices will match requirements in order
+ * e.g. 1 Device - 1 Requirement, 2 Device - 2 Requirement etc.
+ * 2) ASEs should be active only if best (according to priority list) full
+ * scenarion will be covered.
+ * 3) ASEs should be filled according to performance profile.
+ */
+ for (const auto& ent : (*audio_set_conf).confs) {
+ DLOG(INFO) << __func__
+ << " Looking for configuration: " << audio_set_conf->name
+ << " - "
+ << (ent.direction == types::kLeAudioDirectionSink ? "snk"
+ : "src");
+
+ uint8_t required_device_cnt = ent.device_cnt;
+ uint8_t max_required_ase_per_dev =
+ ent.ase_cnt / ent.device_cnt + (ent.ase_cnt % ent.device_cnt);
+ uint8_t active_ase_num = 0;
+ auto strategy = ent.strategy;
+
+ DLOG(INFO) << __func__ << " Number of devices: " << +required_device_cnt
+ << " number of ASEs: " << +ent.ase_cnt
+ << " Max ASE per device: " << +max_required_ase_per_dev
+ << " strategy: " << static_cast<int>(strategy);
+
+ for (auto* device = GetFirstDeviceWithActiveContext(context_type);
+ device != nullptr && required_device_cnt > 0;
+ device = GetNextDeviceWithActiveContext(device, context_type)) {
+ /* Skip if device has ASE configured in this direction already */
+
+ if (device->ases_.empty()) continue;
+
+ if (!device->IsCodecConfigurationSupported(ent.direction, ent.codec))
+ continue;
+
+ int needed_ase = std::min(static_cast<int>(max_required_ase_per_dev),
+ static_cast<int>(ent.ase_cnt - active_ase_num));
+
+ /* If we required more ASEs per device which means we would like to
+ * create more CISes to one device, we should also check the allocation
+ * if it allows us to do this.
+ */
+
+ types::AudioLocations audio_locations = 0;
+ /* Check direction and if audio location allows to create more cise */
+ if (ent.direction == types::kLeAudioDirectionSink)
+ audio_locations = device->snk_audio_locations_;
+ else
+ audio_locations = device->src_audio_locations_;
+
+ /* TODO Make it no Lc3 specific */
+ if (!CheckIfStrategySupported(
+ strategy, audio_locations,
+ std::get<LeAudioLc3Config>(ent.codec.config).GetChannelCount(),
+ device->GetLc3SupportedChannelCount(ent.direction))) {
+ DLOG(INFO) << __func__ << " insufficient device audio allocation: "
+ << audio_locations;
+ continue;
+ }
+
+ for (auto& ase : device->ases_) {
+ if (ase.direction != ent.direction) continue;
+
+ active_ase_num++;
+ needed_ase--;
+
+ if (needed_ase == 0) break;
+ }
+
+ required_device_cnt--;
+ }
+
+ if (required_device_cnt > 0) {
+ /* Don't left any active devices if requirements are not met */
+ DLOG(INFO) << __func__ << " could not configure all the devices";
+ return false;
+ }
+ }
+
+ DLOG(INFO) << "Choosed ASE Configuration for group: " << this->group_id_
+ << " configuration: " << audio_set_conf->name;
+ return true;
+}
+
+uint32_t GetFirstLeft(const types::AudioLocations audio_locations) {
+ uint32_t audio_location_ulong = audio_locations.to_ulong();
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationFrontLeft)
+ return codec_spec_conf::kLeAudioLocationFrontLeft;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationBackLeft)
+ return codec_spec_conf::kLeAudioLocationBackLeft;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationFrontLeftOfCenter)
+ return codec_spec_conf::kLeAudioLocationFrontLeftOfCenter;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationSideLeft)
+ return codec_spec_conf::kLeAudioLocationSideLeft;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationTopFrontLeft)
+ return codec_spec_conf::kLeAudioLocationTopFrontLeft;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationTopBackLeft)
+ return codec_spec_conf::kLeAudioLocationTopBackLeft;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationTopSideLeft)
+ return codec_spec_conf::kLeAudioLocationTopSideLeft;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationBottomFrontLeft)
+ return codec_spec_conf::kLeAudioLocationBottomFrontLeft;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationFrontLeftWide)
+ return codec_spec_conf::kLeAudioLocationFrontLeftWide;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationLeftSurround)
+ return codec_spec_conf::kLeAudioLocationLeftSurround;
+
+ LOG_ASSERT(0) << __func__ << " shall not happen";
+ return 0;
+}
+
+uint32_t GetFirstRight(const types::AudioLocations audio_locations) {
+ uint32_t audio_location_ulong = audio_locations.to_ulong();
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationFrontRight)
+ return codec_spec_conf::kLeAudioLocationFrontRight;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationBackRight)
+ return codec_spec_conf::kLeAudioLocationBackRight;
+
+ if (audio_location_ulong &
+ codec_spec_conf::kLeAudioLocationFrontRightOfCenter)
+ return codec_spec_conf::kLeAudioLocationFrontRightOfCenter;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationSideRight)
+ return codec_spec_conf::kLeAudioLocationSideRight;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationTopFrontRight)
+ return codec_spec_conf::kLeAudioLocationTopFrontRight;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationTopBackRight)
+ return codec_spec_conf::kLeAudioLocationTopBackRight;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationTopSideRight)
+ return codec_spec_conf::kLeAudioLocationTopSideRight;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationBottomFrontRight)
+ return codec_spec_conf::kLeAudioLocationBottomFrontRight;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationFrontRightWide)
+ return codec_spec_conf::kLeAudioLocationFrontRightWide;
+
+ if (audio_location_ulong & codec_spec_conf::kLeAudioLocationRightSurround)
+ return codec_spec_conf::kLeAudioLocationRightSurround;
+
+ LOG_ASSERT(0) << __func__ << " shall not happen";
+ return 0;
+}
+
+uint32_t PickAudioLocation(types::LeAudioConfigurationStrategy strategy,
+ types::AudioLocations audio_locations,
+ types::AudioLocations* group_audio_locations) {
+ DLOG(INFO) << __func__ << " strategy: " << (int)strategy
+ << " locations: " << +audio_locations.to_ulong()
+ << " group locations: " << +group_audio_locations->to_ulong();
+
+ switch (strategy) {
+ case types::LeAudioConfigurationStrategy::MONO_ONE_CIS_PER_DEVICE:
+ case types::LeAudioConfigurationStrategy::STEREO_TWO_CISES_PER_DEVICE:
+ if ((audio_locations.to_ulong() &
+ codec_spec_conf::kLeAudioLocationAnyLeft) &&
+ !(group_audio_locations->to_ulong() &
+ codec_spec_conf::kLeAudioLocationAnyLeft)) {
+ uint32_t left_location = GetFirstLeft(audio_locations);
+ *group_audio_locations |= left_location;
+ return left_location;
+ }
+
+ if ((audio_locations.to_ulong() &
+ codec_spec_conf::kLeAudioLocationAnyRight) &&
+ !(group_audio_locations->to_ulong() &
+ codec_spec_conf::kLeAudioLocationAnyRight)) {
+ uint32_t right_location = GetFirstRight(audio_locations);
+ *group_audio_locations |= right_location;
+ return right_location;
+ }
+ break;
+ case types::LeAudioConfigurationStrategy::STEREO_ONE_CIS_PER_DEVICE:
+ if ((audio_locations.to_ulong() &
+ codec_spec_conf::kLeAudioLocationAnyLeft) &&
+ (audio_locations.to_ulong() &
+ codec_spec_conf::kLeAudioLocationAnyRight)) {
+ uint32_t left_location = GetFirstLeft(audio_locations);
+ uint32_t right_location = GetFirstRight(audio_locations);
+ *group_audio_locations |= left_location | right_location;
+ return left_location | right_location;
+ }
+ break;
+ default:
+ LOG_ASSERT(0) << " Shall never happen ";
+ return 0;
+ }
+
+ LOG_ASSERT(0) << " Shall never happen ";
+ return 0;
+}
+
+bool LeAudioDevice::ConfigureAses(
+ const le_audio::set_configurations::SetConfiguration& ent,
+ types::LeAudioContextType context_type,
+ uint8_t* number_of_already_active_group_ase,
+ types::AudioLocations& group_snk_audio_locations,
+ types::AudioLocations& group_src_audio_locations, bool reconnect) {
+ struct ase* ase = GetFirstInactiveAse(ent.direction, reconnect);
+ if (!ase) return false;
+
+ uint8_t active_ases = *number_of_already_active_group_ase;
+ uint8_t max_required_ase_per_dev =
+ ent.ase_cnt / ent.device_cnt + (ent.ase_cnt % ent.device_cnt);
+ le_audio::types::LeAudioConfigurationStrategy strategy = ent.strategy;
+
+ bool is_codec_supported =
+ IsCodecConfigurationSupported(ent.direction, ent.codec);
+ if (!is_codec_supported) return false;
+
+ int needed_ase = std::min((int)(max_required_ase_per_dev),
+ (int)(ent.ase_cnt - active_ases));
+
+ types::AudioLocations audio_locations = 0;
+ types::AudioLocations* group_audio_locations;
+ /* Check direction and if audio location allows to create more cise */
+ if (ent.direction == types::kLeAudioDirectionSink) {
+ audio_locations = snk_audio_locations_;
+ group_audio_locations = &group_snk_audio_locations;
+ } else {
+ audio_locations = src_audio_locations_;
+ group_audio_locations = &group_src_audio_locations;
+ }
+
+ for (; needed_ase && ase; needed_ase--) {
+ ase->active = true;
+ active_ases++;
+
+ if (ase->state == AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED)
+ ase->reconfigure = true;
+
+ ase->codec_id = ent.codec.id;
+ /* TODO: find better way to not use LC3 explicitly */
+ ase->codec_config = std::get<LeAudioLc3Config>(ent.codec.config);
+
+ /*Let's choose audio channel allocation if not set */
+ ase->codec_config.audio_channel_allocation =
+ PickAudioLocation(strategy, audio_locations, group_audio_locations);
+
+ ase->max_sdu_size = codec_spec_caps::GetAudioChannelCounts(
+ ase->codec_config.audio_channel_allocation) *
+ ase->codec_config.octets_per_codec_frame;
+
+ /* Append additional metadata */
+ std::vector<uint8_t> metadata;
+ metadata.resize(3 + 1);
+ uint8_t* metadata_buf = metadata.data();
+ UINT8_TO_STREAM(metadata_buf, 3);
+ UINT8_TO_STREAM(metadata_buf,
+ types::kLeAudioMetadataTypeStreamingAudioContext);
+ UINT16_TO_STREAM(metadata_buf, static_cast<uint16_t>(context_type));
+
+ ase->metadata = std::move(metadata);
+
+ DLOG(INFO) << __func__ << " device=" << address_
+ << ", activated ASE id=" << +ase->id
+ << ", direction=" << +ase->direction
+ << ", max_sdu_size=" << +ase->max_sdu_size
+ << ", cis_id=" << +ase->cis_id;
+
+ ase = GetFirstInactiveAse(ent.direction, reconnect);
+ }
+
+ *number_of_already_active_group_ase = active_ases;
+ return true;
+}
+
+/* This method should choose aproperiate ASEs to be active and set a cached
+ * configuration for codec and qos.
+ */
+bool LeAudioDeviceGroup::ConfigureAses(
+ const set_configurations::AudioSetConfiguration* audio_set_conf,
+ types::LeAudioContextType context_type) {
+ if (!set_configurations::check_if_may_cover_scenario(
+ audio_set_conf, NumOfConnected(context_type)))
+ return false;
+
+ /* TODO For now: set ase if matching with first pac.
+ * 1) We assume as well that devices will match requirements in order
+ * e.g. 1 Device - 1 Requirement, 2 Device - 2 Requirement etc.
+ * 2) ASEs should be active only if best (according to priority list) full
+ * scenarion will be covered.
+ * 3) ASEs should be filled according to performance profile.
+ */
+
+ types::AudioLocations group_snk_audio_locations = 0;
+ types::AudioLocations group_src_audio_locations = 0;
+
+ for (const auto& ent : (*audio_set_conf).confs) {
+ DLOG(INFO) << __func__
+ << " Looking for requirements: " << audio_set_conf->name << " - "
+ << (ent.direction == 1 ? "snk" : "src");
+
+ uint8_t required_device_cnt = ent.device_cnt;
+ uint8_t max_required_ase_per_dev =
+ ent.ase_cnt / ent.device_cnt + (ent.ase_cnt % ent.device_cnt);
+ uint8_t active_ase_num = 0;
+ le_audio::types::LeAudioConfigurationStrategy strategy = ent.strategy;
+
+ DLOG(INFO) << __func__ << " Number of devices: " << +required_device_cnt
+ << " number of ASEs: " << +ent.ase_cnt
+ << " Max ASE per device: " << +max_required_ase_per_dev
+ << " strategy: " << (int)strategy;
+
+ for (auto* device = GetFirstDeviceWithActiveContext(context_type);
+ device != nullptr && required_device_cnt > 0;
+ device = GetNextDeviceWithActiveContext(device, context_type)) {
+ /* Skip if device has ASE configured in this direction already */
+ if (device->GetFirstActiveAseByDirection(ent.direction)) continue;
+
+ if (!device->ConfigureAses(ent, context_type, &active_ase_num,
+ group_snk_audio_locations,
+ group_src_audio_locations))
+ continue;
+
+ required_device_cnt--;
+ }
+
+ if (required_device_cnt > 0) {
+ /* Don't left any active devices if requirements are not met */
+ LOG(ERROR) << __func__ << " could not configure all the devices";
+ Deactivate();
+ return false;
+ }
+ }
+
+ LOG(INFO) << "Choosed ASE Configuration for group: " << this->group_id_
+ << " configuration: " << audio_set_conf->name;
+
+ active_context_type_ = context_type;
+ return true;
+}
+
+const set_configurations::AudioSetConfiguration*
+LeAudioDeviceGroup::GetActiveConfiguration(void) {
+ return active_context_to_configuration_map[active_context_type_];
+}
+AudioContexts LeAudioDeviceGroup::GetActiveContexts(void) {
+ return active_contexts_mask_;
+}
+
+std::optional<LeAudioCodecConfiguration>
+LeAudioDeviceGroup::GetCodecConfigurationByDirection(
+ types::LeAudioContextType group_context_type, uint8_t direction) {
+ const set_configurations::AudioSetConfiguration* audio_set_conf =
+ active_context_to_configuration_map[group_context_type];
+ LeAudioCodecConfiguration group_config = {0, 0, 0, 0};
+ if (!audio_set_conf) return std::nullopt;
+
+ for (const auto& conf : audio_set_conf->confs) {
+ if (conf.direction != direction) continue;
+
+ if (group_config.sample_rate != 0 &&
+ conf.codec.GetConfigSamplingFrequency() != group_config.sample_rate) {
+ LOG(WARNING) << __func__
+ << ", stream configuration could not be "
+ "determined (sampling frequency differs) for direction: "
+ << loghex(direction);
+ return std::nullopt;
+ }
+ group_config.sample_rate = conf.codec.GetConfigSamplingFrequency();
+
+ if (group_config.data_interval_us != 0 &&
+ conf.codec.GetConfigDataIntervalUs() != group_config.data_interval_us) {
+ LOG(WARNING) << __func__
+ << ", stream configuration could not be "
+ "determined (data interval differs) for direction: "
+ << loghex(direction);
+ return std::nullopt;
+ }
+ group_config.data_interval_us = conf.codec.GetConfigDataIntervalUs();
+
+ if (group_config.bits_per_sample != 0 &&
+ conf.codec.GetConfigBitsPerSample() != group_config.bits_per_sample) {
+ LOG(WARNING) << __func__
+ << ", stream configuration could not be "
+ "determined (bits per sample differs) for direction: "
+ << loghex(direction);
+ return std::nullopt;
+ }
+ group_config.bits_per_sample = conf.codec.GetConfigBitsPerSample();
+
+ group_config.num_channels +=
+ conf.codec.GetConfigChannelCount() * conf.device_cnt;
+ }
+
+ if (group_config.IsInvalid()) return std::nullopt;
+
+ return group_config;
+}
+
+types::LeAudioContextType LeAudioDeviceGroup::GetCurrentContextType(void) {
+ return active_context_type_;
+}
+
+const set_configurations::AudioSetConfiguration*
+LeAudioDeviceGroup::FindFirstSupportedConfiguration(
+ LeAudioContextType context_type) {
+ const set_configurations::AudioSetConfigurations* confs =
+ set_configurations::get_confs_by_type(context_type);
+
+ DLOG(INFO) << __func__ << " context type: " << (int)context_type
+ << " number of connected devices: " << NumOfConnected();
+
+ /* Filter out device set for all scenarios */
+ if (!set_configurations::check_if_may_cover_scenario(confs,
+ NumOfConnected())) {
+ LOG(ERROR) << __func__ << ", group is unable to cover scenario";
+ return nullptr;
+ }
+
+ /* Filter out device set for each end every scenario */
+
+ for (const auto& conf : *confs) {
+ if (IsConfigurationSupported(conf, context_type)) {
+ DLOG(INFO) << __func__ << " found: " << conf->name;
+ return conf;
+ }
+ }
+
+ return nullptr;
+}
+
+/* This method should choose aproperiate ASEs to be active and set a cached
+ * configuration for codec and qos.
+ */
+bool LeAudioDeviceGroup::Configure(LeAudioContextType context_type) {
+ const set_configurations::AudioSetConfiguration* conf =
+ active_context_to_configuration_map[context_type];
+
+ DLOG(INFO) << __func__;
+
+ if (!conf) {
+ LOG(ERROR) << __func__ << ", requested context type: "
+ << loghex(static_cast<uint16_t>(context_type))
+ << ", is in mismatch with cached active contexts";
+ return false;
+ }
+
+ DLOG(INFO) << __func__ << " setting context type: " << int(context_type);
+
+ if (!ConfigureAses(conf, context_type)) {
+ LOG(ERROR) << __func__ << ", requested pick ASE config context type: "
+ << loghex(static_cast<uint16_t>(context_type))
+ << ", is in mismatch with cached active contexts";
+ return false;
+ }
+ return true;
+}
+
+LeAudioDeviceGroup::~LeAudioDeviceGroup(void) { this->Cleanup(); }
+void LeAudioDeviceGroup::Dump(int fd) {
+ std::stringstream stream;
+ auto* active_conf = GetActiveConfiguration();
+
+ stream << " == Group id: " << group_id_ << " == \n"
+ << " state: " << GetState() << "\n"
+ << " target state: " << GetTargetState() << "\n"
+ << " number of devices: " << Size() << "\n"
+ << " number of connected devices: " << NumOfConnected() << "\n"
+ << " active context types: "
+ << loghex(GetActiveContexts().to_ulong()) << "\n"
+ << " current context type: "
+ << static_cast<int>(GetCurrentContextType()) << "\n"
+ << " active stream configuration name: "
+ << (active_conf ? active_conf->name : " not set") << "\n"
+ << " Last used stream configuration: \n"
+ << " valid: " << (stream_conf.valid ? " Yes " : " No") << "\n"
+ << " codec id : " << +(stream_conf.id.coding_format) << "\n"
+ << " name: "
+ << (stream_conf.conf != nullptr ? stream_conf.conf->name : " null ")
+ << "\n"
+ << " number of sinks in the configuration "
+ << stream_conf.sink_num_of_devices << "\n"
+ << " number of sink_streams connected: "
+ << stream_conf.sink_streams.size() << "\n"
+ << " number of sources in the configuration "
+ << stream_conf.source_num_of_devices << "\n"
+ << " number of source_streams connected: "
+ << stream_conf.source_streams.size() << "\n"
+ << " === devices: ===\n";
+
+ dprintf(fd, "%s", stream.str().c_str());
+
+ for (const auto& device_iter : leAudioDevices_) {
+ device_iter.lock()->Dump(fd);
+ }
+}
+
+/* LeAudioDevice Class methods implementation */
+void LeAudioDevice::ClearPACs(void) {
+ snk_pacs_.clear();
+ src_pacs_.clear();
+}
+
+LeAudioDevice::~LeAudioDevice(void) {
+ alarm_free(link_quality_timer);
+ this->ClearPACs();
+}
+
+void LeAudioDevice::RegisterPACs(
+ std::vector<struct types::acs_ac_record>* pac_db,
+ std::vector<struct types::acs_ac_record>* pac_recs) {
+ /* Clear PAC database for characteristic in case if re-read, indicated */
+ if (!pac_db->empty()) {
+ DLOG(INFO) << __func__ << ", upgrade PACs for characteristic";
+ pac_db->clear();
+ }
+
+ /* TODO wrap this logging part with debug flag */
+ for (const struct types::acs_ac_record& pac : *pac_recs) {
+ LOG(INFO) << "Registering PAC"
+ << "\n\tCoding format: " << loghex(pac.codec_id.coding_format)
+ << "\n\tVendor codec company ID: "
+ << loghex(pac.codec_id.vendor_company_id)
+ << "\n\tVendor codec ID: " << loghex(pac.codec_id.vendor_codec_id)
+ << "\n\tCodec spec caps:\n"
+ << pac.codec_spec_caps.ToString() << "\n\tMetadata: "
+ << base::HexEncode(pac.metadata.data(), pac.metadata.size());
+ }
+
+ pac_db->insert(pac_db->begin(), pac_recs->begin(), pac_recs->end());
+}
+
+struct ase* LeAudioDevice::GetAseByValHandle(uint16_t val_hdl) {
+ auto iter = std::find_if(
+ ases_.begin(), ases_.end(),
+ [&val_hdl](const auto& ase) { return ase.hdls.val_hdl == val_hdl; });
+
+ return (iter == ases_.end()) ? nullptr : &(*iter);
+}
+
+struct ase* LeAudioDevice::GetFirstInactiveAseWithState(uint8_t direction,
+ AseState state) {
+ auto iter = std::find_if(
+ ases_.begin(), ases_.end(), [direction, state](const auto& ase) {
+ return ((ase.direction == direction) && (ase.state == state));
+ });
+
+ return (iter == ases_.end()) ? nullptr : &(*iter);
+}
+
+struct ase* LeAudioDevice::GetFirstActiveAse(void) {
+ auto iter = std::find_if(ases_.begin(), ases_.end(),
+ [](const auto& ase) { return ase.active; });
+
+ return (iter == ases_.end()) ? nullptr : &(*iter);
+}
+
+struct ase* LeAudioDevice::GetFirstActiveAseByDirection(uint8_t direction) {
+ auto iter =
+ std::find_if(ases_.begin(), ases_.end(), [direction](const auto& ase) {
+ return (ase.active && (ase.direction == direction));
+ });
+
+ return (iter == ases_.end()) ? nullptr : &(*iter);
+}
+
+struct ase* LeAudioDevice::GetNextActiveAseWithSameDirection(
+ struct ase* base_ase) {
+ auto iter = std::find_if(ases_.begin(), ases_.end(),
+ [&base_ase](auto& ase) { return base_ase == &ase; });
+
+ /* Invalid ase given */
+ if (iter == ases_.end() || std::distance(iter, ases_.end()) < 1)
+ return nullptr;
+
+ iter =
+ std::find_if(std::next(iter, 1), ases_.end(), [&iter](const auto& ase) {
+ return ase.active && (*iter).direction == ase.direction;
+ });
+
+ return (iter == ases_.end()) ? nullptr : &(*iter);
+}
+
+struct ase* LeAudioDevice::GetFirstActiveAseByDataPathState(
+ types::AudioStreamDataPathState state) {
+ auto iter =
+ std::find_if(ases_.begin(), ases_.end(), [state](const auto& ase) {
+ return (ase.active && (ase.data_path_state == state));
+ });
+
+ return (iter == ases_.end()) ? nullptr : &(*iter);
+}
+
+struct ase* LeAudioDevice::GetFirstInactiveAse(uint8_t direction,
+ bool reconnect) {
+ auto iter = std::find_if(ases_.begin(), ases_.end(),
+ [direction, reconnect](const auto& ase) {
+ if (ase.active || (ase.direction != direction))
+ return false;
+
+ if (!reconnect) return true;
+
+ return (ase.cis_id != kInvalidCisId);
+ });
+
+ return (iter == ases_.end()) ? nullptr : &(*iter);
+}
+
+struct ase* LeAudioDevice::GetNextActiveAse(struct ase* base_ase) {
+ auto iter = std::find_if(ases_.begin(), ases_.end(),
+ [&base_ase](auto& ase) { return base_ase == &ase; });
+
+ /* Invalid ase given */
+ if (iter == ases_.end() || std::distance(iter, ases_.end()) < 1)
+ return nullptr;
+
+ iter = std::find_if(std::next(iter, 1), ases_.end(),
+ [](const auto& ase) { return ase.active; });
+
+ return (iter == ases_.end()) ? nullptr : &(*iter);
+}
+
+struct ase* LeAudioDevice::GetAseToMatchBidirectionCis(struct ase* base_ase) {
+ auto iter = std::find_if(ases_.begin(), ases_.end(), [&base_ase](auto& ase) {
+ return (base_ase->cis_conn_hdl == ase.cis_conn_hdl) &&
+ (base_ase->direction != ase.direction);
+ });
+ return (iter == ases_.end()) ? nullptr : &(*iter);
+}
+
+BidirectAsesPair LeAudioDevice::GetAsesByCisConnHdl(uint16_t conn_hdl) {
+ BidirectAsesPair ases;
+
+ for (auto& ase : ases_) {
+ if (ase.cis_conn_hdl == conn_hdl) {
+ if (ase.direction == types::kLeAudioDirectionSink) {
+ ases.sink = &ase;
+ } else {
+ ases.source = &ase;
+ }
+ }
+ }
+
+ return ases;
+}
+
+BidirectAsesPair LeAudioDevice::GetAsesByCisId(uint8_t cis_id) {
+ BidirectAsesPair ases;
+
+ for (auto& ase : ases_) {
+ if (ase.cis_id == cis_id) {
+ if (ase.direction == types::kLeAudioDirectionSink) {
+ ases.sink = &ase;
+ } else {
+ ases.source = &ase;
+ }
+ }
+ }
+
+ return ases;
+}
+
+bool LeAudioDevice::HaveActiveAse(void) {
+ auto iter = std::find_if(ases_.begin(), ases_.end(),
+ [](const auto& ase) { return ase.active; });
+
+ return iter != ases_.end();
+}
+
+bool LeAudioDevice::HaveAnyUnconfiguredAses(void) {
+ /* In configuring state when active in Idle or Configured and reconfigure */
+ auto iter = std::find_if(ases_.begin(), ases_.end(), [](const auto& ase) {
+ if (!ase.active) return false;
+
+ if (ase.state == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE ||
+ ((ase.state == AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED) &&
+ ase.reconfigure))
+ return true;
+
+ return false;
+ });
+
+ return iter != ases_.end();
+}
+
+bool LeAudioDevice::HaveAllActiveAsesSameState(AseState state) {
+ auto iter = std::find_if(
+ ases_.begin(), ases_.end(),
+ [&state](const auto& ase) { return ase.active && (ase.state != state); });
+
+ return iter == ases_.end();
+}
+
+bool LeAudioDevice::IsReadyToCreateStream(void) {
+ auto iter = std::find_if(ases_.begin(), ases_.end(), [](const auto& ase) {
+ if (!ase.active) return false;
+
+ if (ase.direction == types::kLeAudioDirectionSink &&
+ (ase.state != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING &&
+ ase.state != AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING))
+ return true;
+
+ if (ase.direction == types::kLeAudioDirectionSource &&
+ ase.state != AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING)
+ return true;
+
+ return false;
+ });
+
+ return iter == ases_.end();
+}
+
+bool LeAudioDevice::IsReadyToSuspendStream(void) {
+ auto iter = std::find_if(ases_.begin(), ases_.end(), [](const auto& ase) {
+ if (!ase.active) return false;
+
+ if (ase.direction == types::kLeAudioDirectionSink &&
+ ase.state != AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED)
+ return true;
+
+ if (ase.direction == types::kLeAudioDirectionSource &&
+ ase.state != AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING)
+ return true;
+
+ return false;
+ });
+
+ return iter == ases_.end();
+}
+
+bool LeAudioDevice::HaveAllActiveAsesCisEst(void) {
+ auto iter = std::find_if(ases_.begin(), ases_.end(), [](const auto& ase) {
+ return ase.active &&
+ (ase.data_path_state != AudioStreamDataPathState::CIS_ESTABLISHED);
+ });
+
+ return iter == ases_.end();
+}
+
+bool LeAudioDevice::HaveAllAsesCisDisc(void) {
+ auto iter = std::find_if(ases_.begin(), ases_.end(), [](const auto& ase) {
+ return ase.active &&
+ (ase.data_path_state != AudioStreamDataPathState::CIS_ASSIGNED);
+ });
+
+ return iter == ases_.end();
+}
+
+bool LeAudioDevice::HasCisId(uint8_t id) {
+ struct ase* ase = GetFirstActiveAse();
+
+ while (ase) {
+ if (ase->cis_id == id) return true;
+ ase = GetNextActiveAse(ase);
+ }
+
+ return false;
+}
+
+uint8_t LeAudioDevice::GetMatchingBidirectionCisId(
+ const struct types::ase* base_ase) {
+ for (auto& ase : ases_) {
+ auto& cis = ase.cis_id;
+ if (!ase.active) continue;
+
+ int num_cises =
+ std::count_if(ases_.begin(), ases_.end(), [&cis](const auto& iter_ase) {
+ return iter_ase.active && iter_ase.cis_id == cis;
+ });
+
+ /*
+ * If there is only one ASE for device with unique CIS ID and opposite to
+ * direction - it may be bi-directional/completive.
+ */
+ if (num_cises == 1 &&
+ ((base_ase->direction == types::kLeAudioDirectionSink &&
+ ase.direction == types::kLeAudioDirectionSource) ||
+ (base_ase->direction == types::kLeAudioDirectionSource &&
+ ase.direction == types::kLeAudioDirectionSink))) {
+ return ase.cis_id;
+ }
+ }
+
+ return kInvalidCisId;
+}
+
+uint8_t LeAudioDevice::GetLc3SupportedChannelCount(uint8_t direction) {
+ auto& pacs =
+ direction == types::kLeAudioDirectionSink ? snk_pacs_ : src_pacs_;
+
+ if (pacs.size() == 0) {
+ LOG(ERROR) << __func__ << " missing PAC for direction " << +direction;
+ return 0;
+ }
+
+ for (const auto& pac_tuple : pacs) {
+ /* Get PAC records from tuple as second element from tuple */
+ auto& pac_recs = std::get<1>(pac_tuple);
+
+ for (const auto pac : pac_recs) {
+ if (pac.codec_id.coding_format != types::kLeAudioCodingFormatLC3)
+ continue;
+
+ auto supported_channel_count_ltv = pac.codec_spec_caps.Find(
+ codec_spec_caps::kLeAudioCodecLC3TypeAudioChannelCounts);
+
+ return VEC_UINT8_TO_UINT8(supported_channel_count_ltv.value());
+ };
+ }
+
+ return 0;
+}
+
+bool LeAudioDevice::IsCodecConfigurationSupported(
+ uint8_t direction, const CodecCapabilitySetting& codec_capability_setting) {
+ auto& pacs =
+ direction == types::kLeAudioDirectionSink ? snk_pacs_ : src_pacs_;
+
+ if (pacs.size() == 0) {
+ LOG(ERROR) << __func__ << " missing PAC for direction " << +direction;
+ return false;
+ }
+
+ /* TODO: Validate channel locations */
+
+ for (const auto& pac_tuple : pacs) {
+ /* Get PAC records from tuple as second element from tuple */
+ auto& pac_recs = std::get<1>(pac_tuple);
+
+ for (const auto pac : pac_recs) {
+ if (!IsCodecCapabilitySettingSupported(pac, codec_capability_setting))
+ continue;
+
+ return true;
+ };
+ }
+
+ /* Doesn't match required configuration with any PAC */
+ return false;
+}
+
+/**
+ * Returns supported PHY's bitfield
+ */
+uint8_t LeAudioDevice::GetPhyBitmask(void) {
+ uint8_t phy_bitfield = kIsoCigPhy1M;
+
+ if (BTM_IsPhy2mSupported(address_, BT_TRANSPORT_LE))
+ phy_bitfield |= kIsoCigPhy2M;
+
+ return phy_bitfield;
+}
+
+void LeAudioDevice::SetSupportedContexts(AudioContexts snk_contexts,
+ AudioContexts src_contexts) {
+ supp_snk_context_ = snk_contexts;
+ supp_src_context_ = src_contexts;
+}
+
+void LeAudioDevice::Dump(int fd) {
+ std::stringstream stream;
+ stream << " address: " << address_ << "\n"
+ << (conn_id_ == GATT_INVALID_CONN_ID ? " Not connected "
+ : " Connected conn_id =")
+ << (conn_id_ == GATT_INVALID_CONN_ID ? "" : std::to_string(conn_id_))
+ << "\n"
+ << " set member: " << (csis_member_ ? " Yes" : " No") << "\n"
+ << " known_service_handles_: " << known_service_handles_
+ << "\n"
+ << " notify_connected_after_read_: "
+ << notify_connected_after_read_ << "\n"
+ << " removing_device_: " << removing_device_ << "\n"
+ << " first_connection_: " << first_connection_ << "\n"
+ << " encrypted_: " << encrypted_ << "\n"
+ << " connecting_actively_: " << connecting_actively_ << "\n";
+
+ dprintf(fd, "%s", stream.str().c_str());
+}
+
+AudioContexts LeAudioDevice::GetAvailableContexts(void) {
+ return avail_snk_contexts_ | avail_src_contexts_;
+}
+
+/* Returns XOR of updated sink and source bitset context types */
+AudioContexts LeAudioDevice::SetAvailableContexts(AudioContexts snk_contexts,
+ AudioContexts src_contexts) {
+ AudioContexts updated_contexts;
+
+ updated_contexts = snk_contexts ^ avail_snk_contexts_;
+ updated_contexts |= src_contexts ^ avail_src_contexts_;
+
+ DLOG(INFO) << __func__
+ << "\n\t avail_snk_contexts_: " << avail_snk_contexts_.to_string()
+ << "\n\t avail_src_contexts_: " << avail_src_contexts_.to_string()
+ << "\n\t snk_contexts:" << snk_contexts.to_string()
+ << "\n\t src_contexts: " << src_contexts.to_string()
+ << "\n\t updated_contexts: " << updated_contexts.to_string();
+
+ avail_snk_contexts_ = snk_contexts;
+ avail_src_contexts_ = src_contexts;
+
+ return updated_contexts;
+}
+
+void LeAudioDevice::DeactivateAllAses(void) {
+ /* Just clear states and keep previous configuration for use
+ * in case device will get reconnected
+ */
+ for (auto& ase : ases_) {
+ if (ase.active) {
+ ase.state = AseState::BTA_LE_AUDIO_ASE_STATE_IDLE;
+ ase.data_path_state = AudioStreamDataPathState::IDLE;
+ ase.active = false;
+ }
+ }
+}
+
+LeAudioDeviceGroup* LeAudioDeviceGroups::Add(int group_id) {
+ /* Get first free group id */
+ if (FindById(group_id)) {
+ LOG(ERROR) << __func__
+ << ", group already exists, id: " << loghex(group_id);
+ return nullptr;
+ }
+
+ return (groups_.emplace_back(std::make_unique<LeAudioDeviceGroup>(group_id)))
+ .get();
+}
+
+void LeAudioDeviceGroups::Remove(int group_id) {
+ auto iter = std::find_if(
+ groups_.begin(), groups_.end(),
+ [&group_id](auto const& group) { return group->group_id_ == group_id; });
+
+ if (iter == groups_.end()) {
+ LOG(ERROR) << __func__ << ", no such group_id: " << group_id;
+ return;
+ }
+
+ groups_.erase(iter);
+}
+
+LeAudioDeviceGroup* LeAudioDeviceGroups::FindById(int group_id) {
+ auto iter = std::find_if(
+ groups_.begin(), groups_.end(),
+ [&group_id](auto const& group) { return group->group_id_ == group_id; });
+
+ return (iter == groups_.end()) ? nullptr : iter->get();
+}
+
+void LeAudioDeviceGroups::Cleanup(void) {
+ for (auto& g : groups_) {
+ g->Cleanup();
+ }
+
+ groups_.clear();
+}
+
+void LeAudioDeviceGroups::Dump(int fd) {
+ for (auto& g : groups_) {
+ g->Dump(fd);
+ }
+}
+
+bool LeAudioDeviceGroups::IsAnyInTransition(void) {
+ for (auto& g : groups_) {
+ if (g->IsInTransition()) {
+ DLOG(INFO) << __func__ << " group: " << g->group_id_
+ << " is in transition";
+ return true;
+ }
+ }
+ return false;
+}
+
+size_t LeAudioDeviceGroups::Size() { return (groups_.size()); }
+
+std::vector<int> LeAudioDeviceGroups::GetGroupsIds(void) {
+ std::vector<int> result;
+
+ for (auto const& group : groups_) {
+ result.push_back(group->group_id_);
+ }
+
+ return result;
+}
+
+/* LeAudioDevices Class methods implementation */
+void LeAudioDevices::Add(const RawAddress& address, bool first_connection,
+ int group_id) {
+ auto device = FindByAddress(address);
+ if (device != nullptr) {
+ LOG(ERROR) << __func__ << ", address: " << address
+ << " is already assigned to group: " << device->group_id_;
+ return;
+ }
+
+ leAudioDevices_.emplace_back(
+ std::make_shared<LeAudioDevice>(address, first_connection, group_id));
+}
+
+void LeAudioDevices::Remove(const RawAddress& address) {
+ auto iter = std::find_if(leAudioDevices_.begin(), leAudioDevices_.end(),
+ [&address](auto const& leAudioDevice) {
+ return leAudioDevice->address_ == address;
+ });
+
+ if (iter == leAudioDevices_.end()) {
+ LOG(ERROR) << __func__ << ", no such address: " << address;
+ return;
+ }
+
+ leAudioDevices_.erase(iter);
+}
+
+LeAudioDevice* LeAudioDevices::FindByAddress(const RawAddress& address) {
+ auto iter = std::find_if(leAudioDevices_.begin(), leAudioDevices_.end(),
+ [&address](auto const& leAudioDevice) {
+ return leAudioDevice->address_ == address;
+ });
+
+ return (iter == leAudioDevices_.end()) ? nullptr : iter->get();
+}
+
+std::shared_ptr<LeAudioDevice> LeAudioDevices::GetByAddress(
+ const RawAddress& address) {
+ auto iter = std::find_if(leAudioDevices_.begin(), leAudioDevices_.end(),
+ [&address](auto const& leAudioDevice) {
+ return leAudioDevice->address_ == address;
+ });
+
+ return (iter == leAudioDevices_.end()) ? nullptr : *iter;
+}
+
+LeAudioDevice* LeAudioDevices::FindByConnId(uint16_t conn_id) {
+ auto iter = std::find_if(leAudioDevices_.begin(), leAudioDevices_.end(),
+ [&conn_id](auto const& leAudioDevice) {
+ return leAudioDevice->conn_id_ == conn_id;
+ });
+
+ return (iter == leAudioDevices_.end()) ? nullptr : iter->get();
+}
+
+LeAudioDevice* LeAudioDevices::FindByCisConnHdl(const uint16_t conn_hdl) {
+ auto iter = std::find_if(leAudioDevices_.begin(), leAudioDevices_.end(),
+ [&conn_hdl](auto& d) {
+ LeAudioDevice* dev;
+ BidirectAsesPair ases;
+
+ dev = d.get();
+ ases = dev->GetAsesByCisConnHdl(conn_hdl);
+ if (ases.sink || ases.source)
+ return true;
+ else
+ return false;
+ });
+
+ if (iter == leAudioDevices_.end()) return nullptr;
+
+ return iter->get();
+}
+
+size_t LeAudioDevices::Size() { return (leAudioDevices_.size()); }
+
+void LeAudioDevices::Dump(int fd, int group_id) {
+ for (auto const& device : leAudioDevices_) {
+ if (device->group_id_ == group_id) {
+ device->Dump(fd);
+ }
+ }
+}
+
+void LeAudioDevices::Cleanup(void) { leAudioDevices_.clear(); }
+
+} // namespace le_audio
diff --git a/bta/le_audio/devices.h b/bta/le_audio/devices.h
new file mode 100644
index 000000000..464afcd14
--- /dev/null
+++ b/bta/le_audio/devices.h
@@ -0,0 +1,315 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com. Represented by EHIMA
+ * - www.ehima.com
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <map>
+#include <memory>
+#include <optional>
+#include <tuple>
+#include <vector>
+
+#include "bt_types.h"
+#include "bta_groups.h"
+#include "btm_iso_api_types.h"
+#include "client_audio.h"
+#include "gatt_api.h"
+#include "le_audio_types.h"
+#include "osi/include/alarm.h"
+#include "raw_address.h"
+
+namespace le_audio {
+/* Class definitions */
+
+/* LeAudioDevice class represents GATT server device with ASCS, PAC services as
+ * mandatory. Device may contain multiple ASEs, PACs, audio locations. ASEs from
+ * multiple devices may be formed in group.
+ *
+ * Device is created after connection or after storage restoration.
+ *
+ * Active device means that device has at least one ASE which will participate
+ * in any state transition of state machine. ASEs and devices will be activated
+ * according to requested by upper context type.
+ */
+class LeAudioDevice {
+ public:
+ RawAddress address_;
+
+ bool known_service_handles_;
+ bool notify_connected_after_read_;
+ bool removing_device_;
+
+ /* we are making active attempt to connect to this device, 'direct connect'.
+ * This is true only during initial phase of first connection. */
+ bool first_connection_;
+ bool connecting_actively_;
+ uint16_t conn_id_;
+ bool encrypted_;
+ int group_id_;
+ bool csis_member_;
+
+ uint8_t audio_directions_;
+ types::AudioLocations snk_audio_locations_;
+ types::AudioLocations src_audio_locations_;
+
+ types::PublishedAudioCapabilities snk_pacs_;
+ types::PublishedAudioCapabilities src_pacs_;
+
+ struct types::hdl_pair snk_audio_locations_hdls_;
+ struct types::hdl_pair src_audio_locations_hdls_;
+ struct types::hdl_pair audio_avail_hdls_;
+ struct types::hdl_pair audio_supp_cont_hdls_;
+ std::vector<struct types::ase> ases_;
+ struct types::hdl_pair ctp_hdls_;
+
+ alarm_t* link_quality_timer;
+ uint16_t link_quality_timer_data;
+
+ LeAudioDevice(const RawAddress& address_, bool first_connection,
+ int group_id = bluetooth::groups::kGroupUnknown)
+ : address_(address_),
+ known_service_handles_(false),
+ notify_connected_after_read_(false),
+ removing_device_(false),
+ first_connection_(first_connection),
+ connecting_actively_(first_connection),
+ conn_id_(GATT_INVALID_CONN_ID),
+ encrypted_(false),
+ group_id_(group_id),
+ csis_member_(false),
+ link_quality_timer(nullptr) {}
+ ~LeAudioDevice(void);
+
+ void ClearPACs(void);
+ void RegisterPACs(std::vector<struct types::acs_ac_record>* apr_db,
+ std::vector<struct types::acs_ac_record>* apr);
+ struct types::ase* GetAseByValHandle(uint16_t val_hdl);
+ struct types::ase* GetFirstActiveAse(void);
+ struct types::ase* GetFirstActiveAseByDirection(uint8_t direction);
+ struct types::ase* GetNextActiveAseWithSameDirection(
+ struct types::ase* base_ase);
+ struct types::ase* GetFirstActiveAseByDataPathState(
+ types::AudioStreamDataPathState state);
+ struct types::ase* GetFirstInactiveAse(uint8_t direction,
+ bool reconnect = false);
+ struct types::ase* GetFirstInactiveAseWithState(uint8_t direction,
+ types::AseState state);
+ struct types::ase* GetNextActiveAse(struct types::ase* ase);
+ struct types::ase* GetAseToMatchBidirectionCis(struct types::ase* ase);
+ types::BidirectAsesPair GetAsesByCisConnHdl(uint16_t conn_hdl);
+ types::BidirectAsesPair GetAsesByCisId(uint8_t cis_id);
+ bool HaveActiveAse(void);
+ bool HaveAllActiveAsesSameState(types::AseState state);
+ bool HaveAnyUnconfiguredAses(void);
+ bool IsReadyToCreateStream(void);
+ bool IsReadyToSuspendStream(void);
+ bool HaveAllActiveAsesCisEst(void);
+ bool HaveAllAsesCisDisc(void);
+ bool HasCisId(uint8_t id);
+ uint8_t GetMatchingBidirectionCisId(const struct types::ase* base_ase);
+ bool IsCodecConfigurationSupported(
+ uint8_t direction, const set_configurations::CodecCapabilitySetting&
+ codec_capability_setting);
+ uint8_t GetLc3SupportedChannelCount(uint8_t direction);
+ uint8_t GetPhyBitmask(void);
+ bool ConfigureAses(const le_audio::set_configurations::SetConfiguration& ent,
+ types::LeAudioContextType context_type,
+ uint8_t* number_of_already_active_group_ase,
+ types::AudioLocations& group_snk_audio_locations,
+ types::AudioLocations& group_src_audio_locations,
+ bool reconnect = false);
+ void SetSupportedContexts(types::AudioContexts snk_contexts,
+ types::AudioContexts src_contexts);
+ types::AudioContexts GetAvailableContexts(void);
+ types::AudioContexts SetAvailableContexts(types::AudioContexts snk_cont_val,
+ types::AudioContexts src_cont_val);
+ void DeactivateAllAses(void);
+ void Dump(int fd);
+
+ private:
+ types::AudioContexts avail_snk_contexts_;
+ types::AudioContexts avail_src_contexts_;
+ types::AudioContexts supp_snk_context_;
+ types::AudioContexts supp_src_context_;
+};
+
+/* LeAudioDevices class represents a wraper helper over all devices in le audio
+ * implementation. It allows to operate on device from a list (vector container)
+ * using determinants like address, connection id etc.
+ */
+class LeAudioDevices {
+ public:
+ void Add(const RawAddress& address, bool first_connection,
+ int group_id = bluetooth::groups::kGroupUnknown);
+ void Remove(const RawAddress& address);
+ LeAudioDevice* FindByAddress(const RawAddress& address);
+ std::shared_ptr<LeAudioDevice> GetByAddress(const RawAddress& address);
+ LeAudioDevice* FindByConnId(uint16_t conn_id);
+ LeAudioDevice* FindByCisConnHdl(const uint16_t conn_hdl);
+ size_t Size(void);
+ void Dump(int fd, int group_id);
+ void Cleanup(void);
+
+ private:
+ std::vector<std::shared_ptr<LeAudioDevice>> leAudioDevices_;
+};
+
+/* LeAudioDeviceGroup class represents group of LeAudioDevices and allows to
+ * perform operations on them. Group states are ASE states due to nature of
+ * group which operates finally of ASEs.
+ *
+ * Group is created after adding a node to new group id (which is not on list).
+ */
+
+class LeAudioDeviceGroup {
+ public:
+ const int group_id_;
+ bool cig_created_;
+
+ struct stream_configuration stream_conf;
+
+ uint8_t audio_directions_;
+ types::AudioLocations snk_audio_locations_;
+ types::AudioLocations src_audio_locations_;
+
+ /* The below is used only for Coordinate sets */
+ types::CsisDiscoveryState csis_discovery_state_;
+ explicit LeAudioDeviceGroup(const int group_id)
+ : group_id_(group_id),
+ cig_created_(false),
+ transport_latency_mtos_(0),
+ transport_latency_stom_(0),
+ target_state_(types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE),
+ current_state_(types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {
+ stream_conf.valid = false;
+ stream_conf.conf = nullptr;
+ stream_conf.sink_num_of_devices = 0;
+ stream_conf.source_num_of_devices = 0;
+ }
+ ~LeAudioDeviceGroup(void);
+
+ void AddNode(const std::shared_ptr<LeAudioDevice>& leAudioDevice);
+ void RemoveNode(const std::shared_ptr<LeAudioDevice>& leAudioDevice);
+ bool IsEmpty(void);
+ bool IsAnyDeviceConnected(void);
+ int Size(void);
+ int NumOfConnected(
+ types::LeAudioContextType context_type = types::LeAudioContextType::RFU);
+ void Deactivate(void);
+ void Cleanup(void);
+ LeAudioDevice* GetFirstDevice(void);
+ LeAudioDevice* GetFirstDeviceWithActiveContext(
+ types::LeAudioContextType context_type);
+ LeAudioDevice* GetNextDevice(LeAudioDevice* leAudioDevice);
+ LeAudioDevice* GetNextDeviceWithActiveContext(
+ LeAudioDevice* leAudioDevice, types::LeAudioContextType context_type);
+ LeAudioDevice* GetFirstActiveDevice(void);
+ LeAudioDevice* GetNextActiveDevice(LeAudioDevice* leAudioDevice);
+ bool IsDeviceInTheGroup(LeAudioDevice* leAudioDevice);
+ bool HaveAllActiveDevicesAsesTheSameState(types::AseState state);
+ bool IsGroupStreamReady(void);
+ bool HaveAllActiveDevicesCisDisc(void);
+ uint8_t GetFirstFreeCisId(void);
+ bool Configure(types::LeAudioContextType context_type);
+ bool SetContextType(types::LeAudioContextType context_type);
+ types::LeAudioContextType GetContextType(void);
+ uint32_t GetSduInterval(uint8_t direction);
+ uint8_t GetSCA(void);
+ uint8_t GetPacking(void);
+ uint8_t GetFraming(void);
+ uint8_t GetTargetLatency(void);
+ uint16_t GetMaxTransportLatencyStom(void);
+ uint16_t GetMaxTransportLatencyMtos(void);
+ void SetTransportLatency(uint8_t direction, uint16_t transport_latency);
+ uint8_t GetPhyBitmask(uint8_t direction);
+ uint8_t GetTargetPhy(uint8_t direction);
+ bool GetPresentationDelay(uint32_t* delay, uint8_t direction);
+ uint16_t GetRemoteDelay(uint8_t direction);
+ std::optional<types::AudioContexts> UpdateActiveContextsMap(
+ types::AudioContexts contexts);
+ std::optional<types::AudioContexts> UpdateActiveContextsMap(void);
+ bool ReloadAudioLocations(void);
+ const set_configurations::AudioSetConfiguration* GetActiveConfiguration(void);
+ types::LeAudioContextType GetCurrentContextType(void);
+ types::AudioContexts GetActiveContexts(void);
+ std::optional<LeAudioCodecConfiguration> GetCodecConfigurationByDirection(
+ types::LeAudioContextType group_context_type, uint8_t direction);
+
+ inline types::AseState GetState(void) const { return current_state_; }
+ void SetState(types::AseState state) {
+ LOG(INFO) << __func__ << " current state: " << current_state_
+ << " new state: " << state;
+ current_state_ = state;
+ }
+
+ inline types::AseState GetTargetState(void) const { return target_state_; }
+ void SetTargetState(types::AseState state) {
+ LOG(INFO) << __func__ << " target state: " << target_state_
+ << " new target state: " << state;
+ target_state_ = state;
+ }
+
+ bool IsInTransition(void);
+ bool IsReleasing(void);
+ void Dump(int fd);
+
+ private:
+ uint16_t transport_latency_mtos_;
+ uint16_t transport_latency_stom_;
+
+ const set_configurations::AudioSetConfiguration*
+ FindFirstSupportedConfiguration(types::LeAudioContextType context_type);
+ bool ConfigureAses(
+ const set_configurations::AudioSetConfiguration* audio_set_conf,
+ types::LeAudioContextType context_type);
+ bool IsConfigurationSupported(
+ const set_configurations::AudioSetConfiguration* audio_set_configuration,
+ types::LeAudioContextType context_type);
+ uint16_t GetTransportLatency(uint8_t direction);
+
+ /* Mask and table of currently supported contexts */
+ types::LeAudioContextType active_context_type_;
+ types::AudioContexts active_contexts_mask_;
+ std::map<types::LeAudioContextType,
+ const set_configurations::AudioSetConfiguration*>
+ active_context_to_configuration_map;
+
+ types::AseState target_state_;
+ types::AseState current_state_;
+ types::LeAudioContextType context_type_;
+ std::vector<std::weak_ptr<LeAudioDevice>> leAudioDevices_;
+};
+
+/* LeAudioDeviceGroup class represents a wraper helper over all device groups in
+ * le audio implementation. It allows to operate on device group from a list
+ * (vector container) using determinants like id.
+ */
+class LeAudioDeviceGroups {
+ public:
+ LeAudioDeviceGroup* Add(int group_id);
+ void Remove(const int group_id);
+ LeAudioDeviceGroup* FindById(int group_id);
+ std::vector<int> GetGroupsIds(void);
+ size_t Size();
+ bool IsAnyInTransition();
+ void Cleanup(void);
+ void Dump(int fd);
+
+ private:
+ std::vector<std::unique_ptr<LeAudioDeviceGroup>> groups_;
+};
+} // namespace le_audio
diff --git a/bta/le_audio/devices_test.cc b/bta/le_audio/devices_test.cc
new file mode 100644
index 000000000..7b9f5429b
--- /dev/null
+++ b/bta/le_audio/devices_test.cc
@@ -0,0 +1,898 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+#include "devices.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "btm_api_mock.h"
+#include "le_audio_types.h"
+#include "mock_controller.h"
+#include "stack/btm/btm_int_types.h"
+
+tACL_CONN* btm_bda_to_acl(const RawAddress& bda, tBT_TRANSPORT transport) {
+ return nullptr;
+}
+
+namespace bluetooth {
+namespace le_audio {
+namespace internal {
+namespace {
+
+using ::le_audio::LeAudioDevice;
+using ::le_audio::LeAudioDeviceGroup;
+using ::le_audio::LeAudioDevices;
+using ::le_audio::types::AseState;
+using ::le_audio::types::AudioContexts;
+using ::le_audio::types::LeAudioContextType;
+using testing::Test;
+
+RawAddress GetTestAddress(int index) {
+ CHECK_LT(index, UINT8_MAX);
+ RawAddress result = {
+ {0xC0, 0xDE, 0xC0, 0xDE, 0x00, static_cast<uint8_t>(index)}};
+ return result;
+}
+
+class LeAudioDevicesTest : public Test {
+ protected:
+ void SetUp() override {
+ devices_ = new LeAudioDevices();
+ bluetooth::manager::SetMockBtmInterface(&btm_interface);
+ controller::SetMockControllerInterface(&controller_interface_);
+ }
+
+ void TearDown() override {
+ controller::SetMockControllerInterface(nullptr);
+ bluetooth::manager::SetMockBtmInterface(nullptr);
+ delete devices_;
+ }
+
+ LeAudioDevices* devices_ = nullptr;
+ bluetooth::manager::MockBtmInterface btm_interface;
+ controller::MockControllerInterface controller_interface_;
+};
+
+TEST_F(LeAudioDevicesTest, test_add) {
+ RawAddress test_address_0 = GetTestAddress(0);
+ ASSERT_EQ((size_t)0, devices_->Size());
+ devices_->Add(test_address_0, true);
+ ASSERT_EQ((size_t)1, devices_->Size());
+ devices_->Add(GetTestAddress(1), true, 1);
+ ASSERT_EQ((size_t)2, devices_->Size());
+ devices_->Add(test_address_0, true);
+ ASSERT_EQ((size_t)2, devices_->Size());
+ devices_->Add(GetTestAddress(1), true, 2);
+ ASSERT_EQ((size_t)2, devices_->Size());
+}
+
+TEST_F(LeAudioDevicesTest, test_remove) {
+ RawAddress test_address_0 = GetTestAddress(0);
+ devices_->Add(test_address_0, true);
+ RawAddress test_address_1 = GetTestAddress(1);
+ devices_->Add(test_address_1, true);
+ RawAddress test_address_2 = GetTestAddress(2);
+ devices_->Add(test_address_2, true);
+ ASSERT_EQ((size_t)3, devices_->Size());
+ devices_->Remove(test_address_0);
+ ASSERT_EQ((size_t)2, devices_->Size());
+ devices_->Remove(GetTestAddress(3));
+ ASSERT_EQ((size_t)2, devices_->Size());
+ devices_->Remove(test_address_0);
+ ASSERT_EQ((size_t)2, devices_->Size());
+}
+
+TEST_F(LeAudioDevicesTest, test_find_by_address_success) {
+ RawAddress test_address_0 = GetTestAddress(0);
+ devices_->Add(test_address_0, true);
+ RawAddress test_address_1 = GetTestAddress(1);
+ devices_->Add(test_address_1, false);
+ RawAddress test_address_2 = GetTestAddress(2);
+ devices_->Add(test_address_2, true);
+ LeAudioDevice* device = devices_->FindByAddress(test_address_1);
+ ASSERT_NE(nullptr, device);
+ ASSERT_EQ(test_address_1, device->address_);
+}
+
+TEST_F(LeAudioDevicesTest, test_find_by_address_failed) {
+ RawAddress test_address_0 = GetTestAddress(0);
+ devices_->Add(test_address_0, true);
+ RawAddress test_address_2 = GetTestAddress(2);
+ devices_->Add(test_address_2, true);
+ LeAudioDevice* device = devices_->FindByAddress(GetTestAddress(1));
+ ASSERT_EQ(nullptr, device);
+}
+
+TEST_F(LeAudioDevicesTest, test_get_by_address_success) {
+ RawAddress test_address_0 = GetTestAddress(0);
+ devices_->Add(test_address_0, true);
+ RawAddress test_address_1 = GetTestAddress(1);
+ devices_->Add(test_address_1, false);
+ RawAddress test_address_2 = GetTestAddress(2);
+ devices_->Add(test_address_2, true);
+ std::shared_ptr<LeAudioDevice> device =
+ devices_->GetByAddress(test_address_1);
+ ASSERT_NE(nullptr, device);
+ ASSERT_EQ(test_address_1, device->address_);
+}
+
+TEST_F(LeAudioDevicesTest, test_get_by_address_failed) {
+ RawAddress test_address_0 = GetTestAddress(0);
+ devices_->Add(test_address_0, true);
+ RawAddress test_address_2 = GetTestAddress(2);
+ devices_->Add(test_address_2, true);
+ std::shared_ptr<LeAudioDevice> device =
+ devices_->GetByAddress(GetTestAddress(1));
+ ASSERT_EQ(nullptr, device);
+}
+
+TEST_F(LeAudioDevicesTest, test_find_by_conn_id_success) {
+ devices_->Add(GetTestAddress(1), true);
+ RawAddress test_address_0 = GetTestAddress(0);
+ devices_->Add(test_address_0, true);
+ devices_->Add(GetTestAddress(4), true);
+ LeAudioDevice* device = devices_->FindByAddress(test_address_0);
+ device->conn_id_ = 0x0005;
+ ASSERT_EQ(device, devices_->FindByConnId(0x0005));
+}
+
+TEST_F(LeAudioDevicesTest, test_find_by_conn_id_failed) {
+ devices_->Add(GetTestAddress(1), true);
+ devices_->Add(GetTestAddress(0), true);
+ devices_->Add(GetTestAddress(4), true);
+ ASSERT_EQ(nullptr, devices_->FindByConnId(0x0006));
+}
+
+/* TODO: Add FindByCisConnHdl test cases (ASE) */
+
+} // namespace
+
+namespace {
+using namespace ::le_audio::codec_spec_caps;
+using namespace ::le_audio::set_configurations;
+using namespace ::le_audio::types;
+
+static const hdl_pair hdl_pair_nil = hdl_pair(0x0000, 0x0000);
+
+enum class Lc3SettingId {
+ _BEGIN,
+ LC3_8_1 = _BEGIN,
+ LC3_8_2,
+ LC3_16_1,
+ LC3_16_2,
+ LC3_24_1,
+ LC3_24_2,
+ LC3_32_1,
+ LC3_32_2,
+ LC3_441_1,
+ LC3_441_2,
+ LC3_48_1,
+ LC3_48_2,
+ LC3_48_3,
+ LC3_48_4,
+ LC3_48_5,
+ LC3_48_6,
+ _END,
+ UNSUPPORTED = _END,
+};
+static constexpr int Lc3SettingIdBegin = static_cast<int>(Lc3SettingId::_BEGIN);
+static constexpr int Lc3SettingIdEnd = static_cast<int>(Lc3SettingId::_END);
+
+bool IsLc3SettingSupported(LeAudioContextType context_type, Lc3SettingId id) {
+ /* Update those values, on any change of codec linked with content type */
+ switch (context_type) {
+ case LeAudioContextType::RINGTONE:
+ case LeAudioContextType::CONVERSATIONAL:
+ if (id == Lc3SettingId::LC3_16_1 || id == Lc3SettingId::LC3_16_2)
+ return true;
+
+ break;
+
+ case LeAudioContextType::MEDIA:
+ if (id == Lc3SettingId::LC3_16_1 || id == Lc3SettingId::LC3_16_2 ||
+ id == Lc3SettingId::LC3_48_4)
+ return true;
+
+ break;
+
+ default:
+ if (id == Lc3SettingId::LC3_16_2) return true;
+
+ break;
+ };
+
+ return false;
+}
+
+static constexpr uint8_t kLeAudioSamplingFreqRfu = 0x0E;
+uint8_t GetSamplingFrequency(Lc3SettingId id) {
+ switch (id) {
+ case Lc3SettingId::LC3_8_1:
+ case Lc3SettingId::LC3_8_2:
+ return ::le_audio::codec_spec_conf::kLeAudioSamplingFreq8000Hz;
+ case Lc3SettingId::LC3_16_1:
+ case Lc3SettingId::LC3_16_2:
+ return ::le_audio::codec_spec_conf::kLeAudioSamplingFreq16000Hz;
+ case Lc3SettingId::LC3_24_1:
+ case Lc3SettingId::LC3_24_2:
+ return ::le_audio::codec_spec_conf::kLeAudioSamplingFreq24000Hz;
+ case Lc3SettingId::LC3_32_1:
+ case Lc3SettingId::LC3_32_2:
+ return ::le_audio::codec_spec_conf::kLeAudioSamplingFreq32000Hz;
+ case Lc3SettingId::LC3_441_1:
+ case Lc3SettingId::LC3_441_2:
+ return ::le_audio::codec_spec_conf::kLeAudioSamplingFreq44100Hz;
+ case Lc3SettingId::LC3_48_1:
+ case Lc3SettingId::LC3_48_2:
+ case Lc3SettingId::LC3_48_3:
+ case Lc3SettingId::LC3_48_4:
+ case Lc3SettingId::LC3_48_5:
+ case Lc3SettingId::LC3_48_6:
+ return ::le_audio::codec_spec_conf::kLeAudioSamplingFreq48000Hz;
+ case Lc3SettingId::UNSUPPORTED:
+ return kLeAudioSamplingFreqRfu;
+ }
+}
+
+static constexpr uint8_t kLeAudioCodecLC3FrameDurRfu = 0x02;
+uint8_t GetFrameDuration(Lc3SettingId id) {
+ switch (id) {
+ case Lc3SettingId::LC3_8_1:
+ case Lc3SettingId::LC3_16_1:
+ case Lc3SettingId::LC3_24_1:
+ case Lc3SettingId::LC3_32_1:
+ case Lc3SettingId::LC3_441_1:
+ case Lc3SettingId::LC3_48_1:
+ case Lc3SettingId::LC3_48_3:
+ case Lc3SettingId::LC3_48_5:
+ return ::le_audio::codec_spec_conf::kLeAudioCodecLC3FrameDur7500us;
+ case Lc3SettingId::LC3_8_2:
+ case Lc3SettingId::LC3_16_2:
+ case Lc3SettingId::LC3_24_2:
+ case Lc3SettingId::LC3_32_2:
+ case Lc3SettingId::LC3_441_2:
+ case Lc3SettingId::LC3_48_2:
+ case Lc3SettingId::LC3_48_4:
+ case Lc3SettingId::LC3_48_6:
+ return ::le_audio::codec_spec_conf::kLeAudioCodecLC3FrameDur10000us;
+ case Lc3SettingId::UNSUPPORTED:
+ return kLeAudioCodecLC3FrameDurRfu;
+ }
+}
+
+static constexpr uint8_t kLeAudioCodecLC3OctetsPerCodecFrameInvalid = 0;
+uint16_t GetOctetsPerCodecFrame(Lc3SettingId id) {
+ switch (id) {
+ case Lc3SettingId::LC3_8_1:
+ return 26;
+ case Lc3SettingId::LC3_8_2:
+ case Lc3SettingId::LC3_16_1:
+ return 30;
+ case Lc3SettingId::LC3_16_2:
+ return 40;
+ case Lc3SettingId::LC3_24_1:
+ return 45;
+ case Lc3SettingId::LC3_24_2:
+ case Lc3SettingId::LC3_32_1:
+ return 60;
+ case Lc3SettingId::LC3_32_2:
+ return 80;
+ case Lc3SettingId::LC3_441_1:
+ return 97;
+ case Lc3SettingId::LC3_441_2:
+ return 130;
+ case Lc3SettingId::LC3_48_1:
+ return 75;
+ case Lc3SettingId::LC3_48_2:
+ return 100;
+ case Lc3SettingId::LC3_48_3:
+ return 90;
+ case Lc3SettingId::LC3_48_4:
+ return 120;
+ case Lc3SettingId::LC3_48_5:
+ return 116;
+ case Lc3SettingId::LC3_48_6:
+ return 155;
+ case Lc3SettingId::UNSUPPORTED:
+ return kLeAudioCodecLC3OctetsPerCodecFrameInvalid;
+ }
+}
+
+class PublishedAudioCapabilitiesBuilder {
+ public:
+ PublishedAudioCapabilitiesBuilder() {}
+
+ void Add(LeAudioCodecId codec_id, uint8_t conf_sampling_frequency,
+ uint8_t conf_frame_duration, uint8_t audio_channel_counts,
+ uint16_t octets_per_frame, uint8_t codec_frames_per_sdu = 0) {
+ uint16_t sampling_frequencies =
+ SamplingFreqConfig2Capability(conf_sampling_frequency);
+ uint8_t frame_durations =
+ FrameDurationConfig2Capability(conf_frame_duration);
+ uint8_t max_codec_frames_per_sdu = codec_frames_per_sdu;
+ uint32_t octets_per_frame_range =
+ octets_per_frame | (octets_per_frame << 16);
+
+ pac_records_.push_back(
+ acs_ac_record({.codec_id = codec_id,
+ .codec_spec_caps = LeAudioLtvMap({
+ {kLeAudioCodecLC3TypeSamplingFreq,
+ UINT16_TO_VEC_UINT8(sampling_frequencies)},
+ {kLeAudioCodecLC3TypeFrameDuration,
+ UINT8_TO_VEC_UINT8(frame_durations)},
+ {kLeAudioCodecLC3TypeAudioChannelCounts,
+ UINT8_TO_VEC_UINT8(audio_channel_counts)},
+ {kLeAudioCodecLC3TypeOctetPerFrame,
+ UINT32_TO_VEC_UINT8(octets_per_frame_range)},
+ {kLeAudioCodecLC3TypeMaxCodecFramesPerSdu,
+ UINT8_TO_VEC_UINT8(max_codec_frames_per_sdu)},
+ }),
+ .metadata = std::vector<uint8_t>(0)}));
+ }
+
+ void Add(const CodecCapabilitySetting& setting,
+ uint8_t audio_channel_counts) {
+ if (setting.id != LeAudioCodecIdLc3) return;
+
+ const LeAudioLc3Config config = std::get<LeAudioLc3Config>(setting.config);
+
+ Add(setting.id, config.sampling_frequency, config.frame_duration,
+ audio_channel_counts, config.octets_per_codec_frame);
+ }
+
+ void Reset() { pac_records_.clear(); }
+
+ PublishedAudioCapabilities Get() {
+ return PublishedAudioCapabilities({{hdl_pair_nil, pac_records_}});
+ }
+
+ private:
+ std::vector<acs_ac_record> pac_records_;
+};
+
+struct TestGroupAseConfigurationData {
+ LeAudioDevice* device;
+ uint8_t audio_channel_counts_snk;
+ uint8_t audio_channel_counts_src;
+
+ uint8_t active_channel_num_snk;
+ uint8_t active_channel_num_src;
+};
+
+class LeAudioAseConfigurationTest : public Test {
+ protected:
+ void SetUp() override {
+ group_ = new LeAudioDeviceGroup(group_id_);
+ bluetooth::manager::SetMockBtmInterface(&btm_interface_);
+ controller::SetMockControllerInterface(&controller_interface_);
+ }
+
+ void TearDown() override {
+ controller::SetMockControllerInterface(nullptr);
+ bluetooth::manager::SetMockBtmInterface(nullptr);
+ devices_.clear();
+ delete group_;
+ }
+
+ LeAudioDevice* AddTestDevice(int snk_ase_num, int src_ase_num,
+ int snk_ase_num_cached = 0,
+ int src_ase_num_cached = 0) {
+ int index = group_->Size() + 1;
+ auto device =
+ (std::make_shared<LeAudioDevice>(GetTestAddress(index), false));
+ devices_.push_back(device);
+ group_->AddNode(device);
+
+ for (int i = 0; i < src_ase_num; i++) {
+ device->ases_.emplace_back(0x0000, 0x0000, kLeAudioDirectionSource);
+ }
+
+ for (int i = 0; i < snk_ase_num; i++) {
+ device->ases_.emplace_back(0x0000, 0x0000, kLeAudioDirectionSink);
+ }
+
+ for (int i = 0; i < src_ase_num_cached; i++) {
+ struct ase ase(0x0000, 0x0000, kLeAudioDirectionSource);
+ ase.state = AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED;
+ device->ases_.push_back(ase);
+ }
+
+ for (int i = 0; i < snk_ase_num_cached; i++) {
+ struct ase ase(0x0000, 0x0000, kLeAudioDirectionSink);
+ ase.state = AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED;
+ device->ases_.push_back(ase);
+ }
+
+ device->SetSupportedContexts((uint16_t)kLeAudioContextAllTypes,
+ (uint16_t)kLeAudioContextAllTypes);
+ device->SetAvailableContexts((uint16_t)kLeAudioContextAllTypes,
+ (uint16_t)kLeAudioContextAllTypes);
+ device->snk_audio_locations_ =
+ ::le_audio::codec_spec_conf::kLeAudioLocationFrontLeft |
+ ::le_audio::codec_spec_conf::kLeAudioLocationFrontRight;
+ device->src_audio_locations_ =
+ ::le_audio::codec_spec_conf::kLeAudioLocationFrontLeft;
+
+ device->conn_id_ = index;
+ return device.get();
+ }
+
+ void TestGroupAseConfigurationVerdict(
+ const TestGroupAseConfigurationData& data) {
+ uint8_t active_channel_num_snk = 0;
+ uint8_t active_channel_num_src = 0;
+
+ bool have_active_ase =
+ data.active_channel_num_snk + data.active_channel_num_src;
+ ASSERT_EQ(have_active_ase, data.device->HaveActiveAse());
+
+ for (ase* ase = data.device->GetFirstActiveAse(); ase;
+ ase = data.device->GetNextActiveAse(ase)) {
+ if (ase->direction == kLeAudioDirectionSink)
+ active_channel_num_snk +=
+ GetAudioChannelCounts(ase->codec_config.audio_channel_allocation);
+ else
+ active_channel_num_src +=
+ GetAudioChannelCounts(ase->codec_config.audio_channel_allocation);
+ }
+
+ ASSERT_EQ(data.active_channel_num_snk, active_channel_num_snk);
+ ASSERT_EQ(data.active_channel_num_src, active_channel_num_src);
+ }
+
+ void SetCisInformationToActiveAse(void) {
+ uint8_t cis_id = 1;
+ uint16_t cis_conn_hdl = 0x0060;
+
+ for (auto& device : devices_) {
+ for (auto& ase : device->ases_) {
+ if (ase.active) {
+ ase.cis_id = cis_id++;
+ ase.cis_conn_hdl = cis_conn_hdl++;
+ }
+ }
+ }
+ }
+
+ void TestSingleAseConfiguration(LeAudioContextType context_type,
+ TestGroupAseConfigurationData* data,
+ uint8_t data_size,
+ const AudioSetConfiguration* audio_set_conf) {
+ // the configuration should fail if there are no active ases expected
+ bool success_expected = data_size > 0;
+ for (int i = 0; i < data_size; i++) {
+ success_expected &=
+ (data[i].active_channel_num_snk + data[i].active_channel_num_src) > 0;
+
+ /* Prepare PAC's */
+ PublishedAudioCapabilitiesBuilder snk_pac_builder, src_pac_builder;
+ for (const auto& entry : (*audio_set_conf).confs) {
+ if (entry.direction == kLeAudioDirectionSink) {
+ snk_pac_builder.Add(entry.codec, data[i].audio_channel_counts_snk);
+ } else {
+ src_pac_builder.Add(entry.codec, data[i].audio_channel_counts_src);
+ }
+ }
+
+ data[i].device->snk_pacs_ = snk_pac_builder.Get();
+ data[i].device->src_pacs_ = src_pac_builder.Get();
+ }
+
+ /* Stimulate update of active context map */
+ group_->UpdateActiveContextsMap(static_cast<uint16_t>(context_type));
+ ASSERT_EQ(success_expected, group_->Configure(context_type));
+
+ for (int i = 0; i < data_size; i++) {
+ TestGroupAseConfigurationVerdict(data[i]);
+ }
+ }
+
+ void TestGroupAseConfiguration(LeAudioContextType context_type,
+ TestGroupAseConfigurationData* data,
+ uint8_t data_size) {
+ const auto* configurations = get_confs_by_type(context_type);
+ for (const auto& audio_set_conf : *configurations) {
+ // the configuration should fail if there are no active ases expected
+ bool success_expected = data_size > 0;
+ for (int i = 0; i < data_size; i++) {
+ success_expected &= (data[i].active_channel_num_snk +
+ data[i].active_channel_num_src) > 0;
+
+ /* Prepare PAC's */
+ PublishedAudioCapabilitiesBuilder snk_pac_builder, src_pac_builder;
+ for (const auto& entry : (*audio_set_conf).confs) {
+ if (entry.direction == kLeAudioDirectionSink) {
+ snk_pac_builder.Add(entry.codec, data[i].audio_channel_counts_snk);
+ } else {
+ src_pac_builder.Add(entry.codec, data[i].audio_channel_counts_src);
+ }
+ }
+
+ data[i].device->snk_pacs_ = snk_pac_builder.Get();
+ data[i].device->src_pacs_ = src_pac_builder.Get();
+ }
+
+ /* Stimulate update of active context map */
+ group_->UpdateActiveContextsMap(static_cast<uint16_t>(context_type));
+ ASSERT_EQ(success_expected, group_->Configure(context_type));
+
+ for (int i = 0; i < data_size; i++) {
+ TestGroupAseConfigurationVerdict(data[i]);
+ }
+
+ group_->Deactivate();
+ TestAsesInactive();
+ }
+ }
+
+ void TestAsesActive(LeAudioCodecId codec_id, uint8_t sampling_frequency,
+ uint8_t frame_duration, uint16_t octets_per_frame) {
+ for (const auto& device : devices_) {
+ for (const auto& ase : device->ases_) {
+ ASSERT_TRUE(ase.active);
+ ASSERT_EQ(ase.codec_id, codec_id);
+
+ /* FIXME: Validate other codec parameters than LC3 if any */
+ ASSERT_EQ(ase.codec_id, LeAudioCodecIdLc3);
+ if (ase.codec_id == LeAudioCodecIdLc3) {
+ ASSERT_EQ(ase.codec_config.sampling_frequency, sampling_frequency);
+ ASSERT_EQ(ase.codec_config.frame_duration, frame_duration);
+ ASSERT_EQ(ase.codec_config.octets_per_codec_frame, octets_per_frame);
+ }
+ }
+ }
+ }
+
+ void TestActiveAses(void) {
+ for (auto& device : devices_) {
+ for (const auto& ase : device->ases_) {
+ if (ase.active) {
+ ASSERT_FALSE(ase.cis_id == ::le_audio::kInvalidCisId);
+ }
+ }
+ }
+ }
+
+ void TestAsesInactivated(const LeAudioDevice* device) {
+ for (const auto& ase : device->ases_) {
+ ASSERT_FALSE(ase.active);
+ }
+ }
+
+ void TestAsesInactive() {
+ for (const auto& device : devices_) {
+ for (const auto& ase : device->ases_) {
+ ASSERT_FALSE(ase.active);
+ }
+ }
+ }
+
+ void TestLc3CodecConfig(LeAudioContextType context_type) {
+ for (int i = Lc3SettingIdBegin; i < Lc3SettingIdEnd; i++) {
+ // test each configuration parameter against valid and invalid value
+ std::array<Lc3SettingId, 2> test_variants = {static_cast<Lc3SettingId>(i),
+ Lc3SettingId::UNSUPPORTED};
+
+ const bool is_lc3_setting_supported =
+ IsLc3SettingSupported(context_type, static_cast<Lc3SettingId>(i));
+
+ for (const auto sf_variant : test_variants) {
+ uint8_t sampling_frequency = GetSamplingFrequency(sf_variant);
+ for (const auto fd_variant : test_variants) {
+ uint8_t frame_duration = GetFrameDuration(fd_variant);
+ for (const auto opcf_variant : test_variants) {
+ uint16_t octets_per_frame = GetOctetsPerCodecFrame(opcf_variant);
+
+ PublishedAudioCapabilitiesBuilder pac_builder;
+ pac_builder.Add(
+ LeAudioCodecIdLc3, sampling_frequency, frame_duration,
+ kLeAudioCodecLC3ChannelCountSingleChannel, octets_per_frame);
+ for (auto& device : devices_) {
+ /* For simplicity configure both PACs with the same
+ parameters*/
+ device->snk_pacs_ = pac_builder.Get();
+ device->src_pacs_ = pac_builder.Get();
+ }
+
+ bool success_expected = is_lc3_setting_supported;
+ if (is_lc3_setting_supported &&
+ (sf_variant == Lc3SettingId::UNSUPPORTED ||
+ fd_variant == Lc3SettingId::UNSUPPORTED ||
+ opcf_variant == Lc3SettingId::UNSUPPORTED)) {
+ success_expected = false;
+ }
+
+ /* Stimulate update of active context map */
+ group_->UpdateActiveContextsMap(
+ static_cast<uint16_t>(context_type));
+ ASSERT_EQ(success_expected, group_->Configure(context_type));
+ if (success_expected) {
+ TestAsesActive(LeAudioCodecIdLc3, sampling_frequency,
+ frame_duration, octets_per_frame);
+ group_->Deactivate();
+ }
+
+ TestAsesInactive();
+ }
+ }
+ }
+ }
+ }
+
+ const int group_id_ = 6;
+ std::vector<std::shared_ptr<LeAudioDevice>> devices_;
+ LeAudioDeviceGroup* group_ = nullptr;
+ bluetooth::manager::MockBtmInterface btm_interface_;
+ controller::MockControllerInterface controller_interface_;
+};
+
+TEST_F(LeAudioAseConfigurationTest, test_mono_speaker_ringtone) {
+ LeAudioDevice* mono_speaker = AddTestDevice(1, 0);
+ TestGroupAseConfigurationData data({mono_speaker,
+ kLeAudioCodecLC3ChannelCountSingleChannel,
+ kLeAudioCodecLC3ChannelCountNone, 1, 0});
+
+ TestGroupAseConfiguration(LeAudioContextType::RINGTONE, &data, 1);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_mono_speaker_conversional) {
+ LeAudioDevice* mono_speaker = AddTestDevice(1, 0);
+ TestGroupAseConfigurationData data({mono_speaker,
+ kLeAudioCodecLC3ChannelCountSingleChannel,
+ kLeAudioCodecLC3ChannelCountNone, 0, 0});
+
+ TestGroupAseConfiguration(LeAudioContextType::CONVERSATIONAL, &data, 1);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_mono_speaker_media) {
+ LeAudioDevice* mono_speaker = AddTestDevice(1, 0);
+ TestGroupAseConfigurationData data({mono_speaker,
+ kLeAudioCodecLC3ChannelCountSingleChannel,
+ kLeAudioCodecLC3ChannelCountNone, 1, 0});
+
+ TestGroupAseConfiguration(LeAudioContextType::MEDIA, &data, 1);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_bounded_headphones_ringtone) {
+ LeAudioDevice* bounded_headphones = AddTestDevice(2, 0);
+ TestGroupAseConfigurationData data({bounded_headphones,
+ kLeAudioCodecLC3ChannelCountTwoChannel,
+ kLeAudioCodecLC3ChannelCountNone, 2, 0});
+
+ TestGroupAseConfiguration(LeAudioContextType::RINGTONE, &data, 1);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_bounded_headphones_conversional) {
+ LeAudioDevice* bounded_headphones = AddTestDevice(2, 0);
+ TestGroupAseConfigurationData data({bounded_headphones,
+ kLeAudioCodecLC3ChannelCountTwoChannel,
+ kLeAudioCodecLC3ChannelCountNone, 0, 0});
+
+ TestGroupAseConfiguration(LeAudioContextType::CONVERSATIONAL, &data, 1);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_bounded_headphones_media) {
+ LeAudioDevice* bounded_headphones = AddTestDevice(2, 0);
+ TestGroupAseConfigurationData data({bounded_headphones,
+ kLeAudioCodecLC3ChannelCountTwoChannel,
+ kLeAudioCodecLC3ChannelCountNone, 2, 0});
+
+ TestGroupAseConfiguration(LeAudioContextType::MEDIA, &data, 1);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_bounded_headset_ringtone) {
+ LeAudioDevice* bounded_headset = AddTestDevice(2, 1);
+ TestGroupAseConfigurationData data(
+ {bounded_headset, kLeAudioCodecLC3ChannelCountTwoChannel,
+ kLeAudioCodecLC3ChannelCountSingleChannel, 2, 0});
+
+ TestGroupAseConfiguration(LeAudioContextType::RINGTONE, &data, 1);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_bounded_headset_conversional) {
+ LeAudioDevice* bounded_headset = AddTestDevice(2, 1);
+ TestGroupAseConfigurationData data(
+ {bounded_headset, kLeAudioCodecLC3ChannelCountTwoChannel,
+ kLeAudioCodecLC3ChannelCountSingleChannel, 2, 1});
+
+ TestGroupAseConfiguration(LeAudioContextType::CONVERSATIONAL, &data, 1);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_bounded_headset_media) {
+ LeAudioDevice* bounded_headset = AddTestDevice(2, 1);
+ TestGroupAseConfigurationData data(
+ {bounded_headset, kLeAudioCodecLC3ChannelCountTwoChannel,
+ kLeAudioCodecLC3ChannelCountSingleChannel, 2, 0});
+
+ TestGroupAseConfiguration(LeAudioContextType::MEDIA, &data, 1);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_earbuds_ringtone) {
+ LeAudioDevice* left = AddTestDevice(1, 1);
+ LeAudioDevice* right = AddTestDevice(1, 1);
+ TestGroupAseConfigurationData data[] = {
+ {left, kLeAudioCodecLC3ChannelCountSingleChannel,
+ kLeAudioCodecLC3ChannelCountSingleChannel, 1, 0},
+ {right, kLeAudioCodecLC3ChannelCountSingleChannel,
+ kLeAudioCodecLC3ChannelCountSingleChannel, 1, 0}};
+
+ TestGroupAseConfiguration(LeAudioContextType::RINGTONE, data, 2);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_earbuds_conversional) {
+ LeAudioDevice* left = AddTestDevice(1, 1);
+ LeAudioDevice* right = AddTestDevice(1, 1);
+ TestGroupAseConfigurationData data[] = {
+ {left, kLeAudioCodecLC3ChannelCountSingleChannel,
+ kLeAudioCodecLC3ChannelCountSingleChannel, 1, 1},
+ {right, kLeAudioCodecLC3ChannelCountSingleChannel,
+ kLeAudioCodecLC3ChannelCountSingleChannel, 1, 0}};
+
+ TestGroupAseConfiguration(LeAudioContextType::CONVERSATIONAL, data, 2);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_earbuds_media) {
+ LeAudioDevice* left = AddTestDevice(1, 1);
+ LeAudioDevice* right = AddTestDevice(1, 1);
+ TestGroupAseConfigurationData data[] = {
+ {left, kLeAudioCodecLC3ChannelCountSingleChannel,
+ kLeAudioCodecLC3ChannelCountSingleChannel, 1, 0},
+ {right, kLeAudioCodecLC3ChannelCountSingleChannel,
+ kLeAudioCodecLC3ChannelCountSingleChannel, 1, 0}};
+
+ TestGroupAseConfiguration(LeAudioContextType::MEDIA, data, 2);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_handsfree_ringtone) {
+ LeAudioDevice* handsfree = AddTestDevice(1, 1);
+ TestGroupAseConfigurationData data(
+ {handsfree, kLeAudioCodecLC3ChannelCountSingleChannel,
+ kLeAudioCodecLC3ChannelCountSingleChannel, 1, 0});
+
+ TestGroupAseConfiguration(LeAudioContextType::RINGTONE, &data, 1);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_handsfree_conversional) {
+ LeAudioDevice* handsfree = AddTestDevice(1, 1);
+ TestGroupAseConfigurationData data(
+ {handsfree, kLeAudioCodecLC3ChannelCountSingleChannel,
+ kLeAudioCodecLC3ChannelCountSingleChannel, 1, 1});
+
+ TestGroupAseConfiguration(LeAudioContextType::CONVERSATIONAL, &data, 1);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_handsfree_full_cached_conversional) {
+ LeAudioDevice* handsfree = AddTestDevice(0, 0, 1, 1);
+ TestGroupAseConfigurationData data(
+ {handsfree, kLeAudioCodecLC3ChannelCountSingleChannel,
+ kLeAudioCodecLC3ChannelCountSingleChannel, 1, 1});
+
+ TestGroupAseConfiguration(LeAudioContextType::CONVERSATIONAL, &data, 1);
+}
+
+TEST_F(LeAudioAseConfigurationTest,
+ test_handsfree_partial_cached_conversional) {
+ LeAudioDevice* handsfree = AddTestDevice(1, 0, 0, 1);
+ TestGroupAseConfigurationData data(
+ {handsfree, kLeAudioCodecLC3ChannelCountSingleChannel,
+ kLeAudioCodecLC3ChannelCountSingleChannel, 1, 1});
+
+ TestGroupAseConfiguration(LeAudioContextType::CONVERSATIONAL, &data, 1);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_handsfree_media) {
+ LeAudioDevice* handsfree = AddTestDevice(1, 1);
+ TestGroupAseConfigurationData data(
+ {handsfree, kLeAudioCodecLC3ChannelCountSingleChannel,
+ kLeAudioCodecLC3ChannelCountSingleChannel, 1, 0});
+
+ TestGroupAseConfiguration(LeAudioContextType::MEDIA, &data, 1);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_lc3_config_ringtone) {
+ AddTestDevice(1, 0);
+
+ TestLc3CodecConfig(LeAudioContextType::RINGTONE);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_lc3_config_conversional) {
+ AddTestDevice(1, 1);
+
+ TestLc3CodecConfig(LeAudioContextType::CONVERSATIONAL);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_lc3_config_media) {
+ AddTestDevice(1, 0);
+
+ TestLc3CodecConfig(LeAudioContextType::MEDIA);
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_unsupported_codec) {
+ const LeAudioCodecId UnsupportedCodecId = {
+ .coding_format = kLeAudioCodingFormatVendorSpecific,
+ .vendor_company_id = 0xBAD,
+ .vendor_codec_id = 0xC0DE,
+ };
+
+ LeAudioDevice* device = AddTestDevice(1, 0);
+
+ PublishedAudioCapabilitiesBuilder pac_builder;
+ pac_builder.Add(UnsupportedCodecId,
+ GetSamplingFrequency(Lc3SettingId::LC3_16_2),
+ GetFrameDuration(Lc3SettingId::LC3_16_2),
+ kLeAudioCodecLC3ChannelCountSingleChannel,
+ GetOctetsPerCodecFrame(Lc3SettingId::LC3_16_2));
+ device->snk_pacs_ = pac_builder.Get();
+ device->src_pacs_ = pac_builder.Get();
+
+ ASSERT_FALSE(group_->Configure(LeAudioContextType::RINGTONE));
+ TestAsesInactive();
+}
+
+TEST_F(LeAudioAseConfigurationTest, test_reconnection_media) {
+ LeAudioDevice* left = AddTestDevice(2, 1);
+ LeAudioDevice* right = AddTestDevice(2, 1);
+
+ TestGroupAseConfigurationData data[] = {
+ {left, kLeAudioCodecLC3ChannelCountSingleChannel,
+ kLeAudioCodecLC3ChannelCountSingleChannel, 1, 0},
+ {right, kLeAudioCodecLC3ChannelCountSingleChannel,
+ kLeAudioCodecLC3ChannelCountSingleChannel, 1, 0}};
+
+ TestSingleAseConfiguration(LeAudioContextType::MEDIA, data, 2,
+ &kDualDev_OneChanStereoSnk_48_4);
+
+ SetCisInformationToActiveAse();
+
+ /* Left got disconnected */
+ left->DeactivateAllAses();
+ TestAsesInactivated(left);
+
+ /* Prepare reconfiguration */
+ uint8_t number_of_active_ases = 1; // Right one
+ auto* ase = right->GetFirstActiveAseByDirection(kLeAudioDirectionSink);
+ ::le_audio::types::AudioLocations group_snk_audio_location =
+ ase->codec_config.audio_channel_allocation;
+ ::le_audio::types::AudioLocations group_src_audio_location =
+ ase->codec_config.audio_channel_allocation;
+
+ /* Get known requirement*/
+ auto* configuration = &kDualDev_OneChanStereoSnk_48_4;
+
+ /* Get entry for the sink direction and use it to set configuration */
+ for (auto& ent : configuration->confs) {
+ if (ent.direction == ::le_audio::types::kLeAudioDirectionSink) {
+ left->ConfigureAses(ent, group_->GetCurrentContextType(),
+ &number_of_active_ases, group_snk_audio_location,
+ group_src_audio_location);
+ }
+ }
+
+ ASSERT_TRUE(number_of_active_ases == 2);
+ ASSERT_TRUE(group_snk_audio_location == kChannelAllocationStereo);
+
+ for (int i = 0; i < 2; i++) {
+ TestGroupAseConfigurationVerdict(data[i]);
+ }
+
+ TestActiveAses();
+}
+} // namespace
+} // namespace internal
+} // namespace le_audio
+} // namespace bluetooth
diff --git a/bta/le_audio/le_audio_client_test.cc b/bta/le_audio/le_audio_client_test.cc
new file mode 100644
index 000000000..84f2b7f45
--- /dev/null
+++ b/bta/le_audio/le_audio_client_test.cc
@@ -0,0 +1,2644 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <chrono>
+
+#include "bta/csis/csis_types.h"
+#include "bta_gatt_api_mock.h"
+#include "bta_gatt_queue_mock.h"
+#include "bta_groups.h"
+#include "bta_le_audio_api.h"
+#include "btif_storage_mock.h"
+#include "btm_api_mock.h"
+#include "btm_iso_api.h"
+#include "common/message_loop_thread.h"
+#include "device/include/controller.h"
+#include "gatt/database_builder.h"
+#include "hardware/bt_gatt_types.h"
+#include "le_audio_types.h"
+#include "mock_controller.h"
+#include "mock_csis_client.h"
+#include "mock_device_groups.h"
+#include "mock_iso_manager.h"
+#include "mock_le_audio_client_audio.h"
+#include "mock_state_machine.h"
+
+using testing::_;
+using testing::AnyNumber;
+using testing::AtLeast;
+using testing::AtMost;
+using testing::DoAll;
+using testing::Invoke;
+using testing::Mock;
+using testing::MockFunction;
+using testing::NotNull;
+using testing::Return;
+using testing::SaveArg;
+using testing::SetArgPointee;
+using testing::Test;
+using testing::WithArg;
+
+using bluetooth::Uuid;
+
+using namespace bluetooth::le_audio;
+
+// Disables most likely false-positives from base::SplitString()
+extern "C" const char* __asan_default_options() {
+ return "detect_container_overflow=0";
+}
+
+std::atomic<int> num_async_tasks;
+bluetooth::common::MessageLoopThread message_loop_thread("test message loop");
+bluetooth::common::MessageLoopThread* get_main_thread() {
+ return &message_loop_thread;
+}
+bt_status_t do_in_main_thread(const base::Location& from_here,
+ base::OnceClosure task) {
+ // Wrap the task with task counter so we could later know if there are
+ // any callbacks scheduled and we should wait before performing some actions
+ if (!message_loop_thread.DoInThread(
+ from_here,
+ base::BindOnce(
+ [](base::OnceClosure task, std::atomic<int>& num_async_tasks) {
+ std::move(task).Run();
+ num_async_tasks--;
+ },
+ std::move(task), std::ref(num_async_tasks)))) {
+ LOG(ERROR) << __func__ << ": failed from " << from_here.ToString();
+ return BT_STATUS_FAIL;
+ }
+ num_async_tasks++;
+ return BT_STATUS_SUCCESS;
+}
+
+static base::MessageLoop* message_loop_;
+base::MessageLoop* get_main_message_loop() { return message_loop_; }
+
+static void init_message_loop_thread() {
+ num_async_tasks = 0;
+ message_loop_thread.StartUp();
+ if (!message_loop_thread.IsRunning()) {
+ FAIL() << "unable to create message loop thread.";
+ }
+
+ if (!message_loop_thread.EnableRealTimeScheduling())
+ LOG(ERROR) << "Unable to set real time scheduling";
+
+ message_loop_ = message_loop_thread.message_loop();
+ if (message_loop_ == nullptr) FAIL() << "unable to get message loop.";
+}
+
+static void cleanup_message_loop_thread() {
+ message_loop_ = nullptr;
+ message_loop_thread.ShutDown();
+}
+
+namespace le_audio {
+namespace {
+class MockLeAudioClientCallbacks
+ : public bluetooth::le_audio::LeAudioClientCallbacks {
+ public:
+ MOCK_METHOD((void), OnConnectionState,
+ (ConnectionState state, const RawAddress& address), (override));
+ MOCK_METHOD((void), OnGroupStatus, (int group_id, GroupStatus group_status),
+ (override));
+ MOCK_METHOD((void), OnGroupNodeStatus,
+ (const RawAddress& bd_addr, int group_id,
+ GroupNodeStatus node_status),
+ (override));
+ MOCK_METHOD((void), OnAudioConf,
+ (uint8_t direction, int group_id, uint32_t snk_audio_location,
+ uint32_t src_audio_location, uint16_t avail_cont),
+ (override));
+};
+
+class UnicastTestNoInit : public Test {
+ protected:
+ void SetUpMockAudioHal() {
+ // Source
+ is_audio_hal_source_acquired = false;
+
+ ON_CALL(mock_audio_source_, Start(_, _))
+ .WillByDefault(
+ [this](const LeAudioCodecConfiguration& codec_configuration,
+ LeAudioClientAudioSinkReceiver* audioReceiver) {
+ audio_sink_receiver_ = audioReceiver;
+ return true;
+ });
+ ON_CALL(mock_audio_source_, Acquire).WillByDefault([this]() -> void* {
+ if (!is_audio_hal_source_acquired) {
+ is_audio_hal_source_acquired = true;
+ return &mock_audio_source_;
+ }
+
+ return nullptr;
+ });
+ ON_CALL(mock_audio_source_, Release)
+ .WillByDefault([this](const void* inst) -> void {
+ if (is_audio_hal_source_acquired) {
+ is_audio_hal_source_acquired = false;
+ }
+ });
+
+ MockLeAudioClientAudioSource::SetMockInstanceForTesting(
+ &mock_audio_source_);
+
+ // Sink
+ is_audio_hal_sink_acquired = false;
+
+ ON_CALL(mock_audio_sink_, Start(_, _))
+ .WillByDefault(
+ [this](const LeAudioCodecConfiguration& codec_configuration,
+ LeAudioClientAudioSourceReceiver* audioReceiver) {
+ audio_source_receiver_ = audioReceiver;
+ return true;
+ });
+ ON_CALL(mock_audio_sink_, Acquire).WillByDefault([this]() -> void* {
+ if (!is_audio_hal_sink_acquired) {
+ is_audio_hal_sink_acquired = true;
+ return &mock_audio_sink_;
+ }
+
+ return nullptr;
+ });
+ ON_CALL(mock_audio_sink_, Release)
+ .WillByDefault([this](const void* inst) -> void {
+ if (is_audio_hal_sink_acquired) {
+ is_audio_hal_sink_acquired = false;
+ }
+ });
+ ON_CALL(mock_audio_sink_, SendData)
+ .WillByDefault([](uint8_t* data, uint16_t size) { return size; });
+
+ MockLeAudioClientAudioSink::SetMockInstanceForTesting(&mock_audio_sink_);
+
+ // HAL
+ ON_CALL(mock_hal_2_1_verifier, Call()).WillByDefault([]() -> bool {
+ return true;
+ });
+ }
+
+ void InjectGroupDeviceRemoved(const RawAddress& address, int group_id) {
+ group_callbacks_->OnGroupMemberRemoved(address, group_id);
+ }
+
+ void InjectGroupDeviceAdded(const RawAddress& address, int group_id) {
+ bluetooth::Uuid uuid = le_audio::uuid::kCapServiceUuid;
+
+ int group_members_num = 0;
+ for (const auto& [addr, id] : groups) {
+ if (id == group_id) group_members_num++;
+ }
+
+ bool first_device = (group_members_num == 1);
+ do_in_main_thread(
+ FROM_HERE,
+ base::BindOnce(
+ [](const RawAddress& addr, int group_id, bluetooth::Uuid uuid,
+ bluetooth::groups::DeviceGroupsCallbacks* group_callbacks,
+ bool first_device) {
+ if (first_device) {
+ group_callbacks->OnGroupAdded(addr, uuid, group_id);
+ } else {
+ group_callbacks->OnGroupMemberAdded(addr, group_id);
+ }
+ },
+ address, group_id, uuid, base::Unretained(this->group_callbacks_),
+ first_device));
+ }
+
+ void InjectConnectedEvent(const RawAddress& address, uint16_t conn_id,
+ tGATT_STATUS status = GATT_SUCCESS) {
+ ASSERT_NE(conn_id, GATT_INVALID_CONN_ID);
+ tBTA_GATTC_OPEN event_data = {
+ .status = status,
+ .conn_id = conn_id,
+ .client_if = gatt_if,
+ .remote_bda = address,
+ .transport = GATT_TRANSPORT_LE,
+ .mtu = 240,
+ };
+
+ ASSERT_NE(peer_devices.count(conn_id), 0u);
+ peer_devices.at(conn_id)->connected = true;
+ do_in_main_thread(
+ FROM_HERE,
+ base::BindOnce(
+ [](tBTA_GATTC_CBACK* gatt_callback, tBTA_GATTC_OPEN event_data) {
+ gatt_callback(BTA_GATTC_OPEN_EVT, (tBTA_GATTC*)&event_data);
+ },
+ base::Unretained(this->gatt_callback), event_data));
+ }
+
+ void InjectDisconnectedEvent(uint16_t conn_id) {
+ ASSERT_NE(conn_id, GATT_INVALID_CONN_ID);
+ ASSERT_NE(peer_devices.count(conn_id), 0u);
+
+ tBTA_GATTC_CLOSE event_data = {
+ .status = GATT_SUCCESS,
+ .conn_id = conn_id,
+ .client_if = gatt_if,
+ .remote_bda = peer_devices.at(conn_id)->addr,
+ .reason = GATT_CONN_TERMINATE_PEER_USER,
+ };
+
+ peer_devices.at(conn_id)->connected = false;
+ do_in_main_thread(
+ FROM_HERE,
+ base::BindOnce(
+ [](tBTA_GATTC_CBACK* gatt_callback, tBTA_GATTC_CLOSE event_data) {
+ gatt_callback(BTA_GATTC_CLOSE_EVT, (tBTA_GATTC*)&event_data);
+ },
+ base::Unretained(this->gatt_callback), event_data));
+ }
+
+ void InjectSearchCompleteEvent(uint16_t conn_id) {
+ ASSERT_NE(conn_id, GATT_INVALID_CONN_ID);
+ tBTA_GATTC_SEARCH_CMPL event_data = {
+ .status = GATT_SUCCESS,
+ .conn_id = conn_id,
+ };
+
+ do_in_main_thread(FROM_HERE,
+ base::BindOnce(
+ [](tBTA_GATTC_CBACK* gatt_callback,
+ tBTA_GATTC_SEARCH_CMPL event_data) {
+ gatt_callback(BTA_GATTC_SEARCH_CMPL_EVT,
+ (tBTA_GATTC*)&event_data);
+ },
+ base::Unretained(this->gatt_callback), event_data));
+ }
+
+ void InjectNotificationEvent(const RawAddress& test_address, uint16_t conn_id,
+ uint16_t handle, std::vector<uint8_t> value) {
+ ASSERT_NE(conn_id, GATT_INVALID_CONN_ID);
+ tBTA_GATTC_NOTIFY event_data = {
+ .conn_id = conn_id,
+ .bda = test_address,
+ .handle = handle,
+ .len = (uint8_t)value.size(),
+ .is_notify = true,
+ };
+
+ std::copy(value.begin(), value.end(), event_data.value);
+ do_in_main_thread(
+ FROM_HERE,
+ base::BindOnce(
+ [](tBTA_GATTC_CBACK* gatt_callback, tBTA_GATTC_NOTIFY event_data) {
+ gatt_callback(BTA_GATTC_NOTIF_EVT, (tBTA_GATTC*)&event_data);
+ },
+ base::Unretained(this->gatt_callback), event_data));
+ }
+
+ void SetUpMockGatt() {
+ // default action for GetCharacteristic function call
+ ON_CALL(mock_gatt_interface_, GetCharacteristic(_, _))
+ .WillByDefault(
+ Invoke([&](uint16_t conn_id,
+ uint16_t handle) -> const gatt::Characteristic* {
+ std::list<gatt::Service>& services =
+ peer_devices.at(conn_id)->services;
+ for (auto const& service : services) {
+ for (auto const& characteristic : service.characteristics) {
+ if (characteristic.value_handle == handle) {
+ return &characteristic;
+ }
+ }
+ }
+
+ return nullptr;
+ }));
+
+ // default action for GetOwningService function call
+ ON_CALL(mock_gatt_interface_, GetOwningService(_, _))
+ .WillByDefault(Invoke(
+ [&](uint16_t conn_id, uint16_t handle) -> const gatt::Service* {
+ std::list<gatt::Service>& services =
+ peer_devices.at(conn_id)->services;
+ for (auto const& service : services) {
+ if (service.handle <= handle && service.end_handle >= handle) {
+ return &service;
+ }
+ }
+
+ return nullptr;
+ }));
+
+ // default action for ServiceSearchRequest function call
+ ON_CALL(mock_gatt_interface_, ServiceSearchRequest(_, _))
+ .WillByDefault(WithArg<0>(Invoke(
+ [&](uint16_t conn_id) { InjectSearchCompleteEvent(conn_id); })));
+
+ // default action for GetServices function call
+ ON_CALL(mock_gatt_interface_, GetServices(_))
+ .WillByDefault(WithArg<0>(
+ Invoke([&](uint16_t conn_id) -> std::list<gatt::Service>* {
+ return &peer_devices.at(conn_id)->services;
+ })));
+
+ // default action for RegisterForNotifications function call
+ ON_CALL(mock_gatt_interface_, RegisterForNotifications(gatt_if, _, _))
+ .WillByDefault(Return(GATT_SUCCESS));
+
+ // default action for DeregisterForNotifications function call
+ ON_CALL(mock_gatt_interface_, DeregisterForNotifications(gatt_if, _, _))
+ .WillByDefault(Return(GATT_SUCCESS));
+
+ // default action for WriteDescriptor function call
+ ON_CALL(mock_gatt_queue_, WriteDescriptor(_, _, _, _, _, _))
+ .WillByDefault(
+ Invoke([](uint16_t conn_id, uint16_t handle,
+ std::vector<uint8_t> value, tGATT_WRITE_TYPE write_type,
+ GATT_WRITE_OP_CB cb, void* cb_data) -> void {
+ if (cb)
+ do_in_main_thread(
+ FROM_HERE, base::BindOnce(
+ [](GATT_WRITE_OP_CB cb, uint16_t conn_id,
+ uint16_t handle, void* cb_data) {
+ cb(conn_id, GATT_SUCCESS, handle, cb_data);
+ },
+ cb, conn_id, handle, cb_data));
+ }));
+
+ global_conn_id = 1;
+ ON_CALL(mock_gatt_interface_, Open(_, _, _, _))
+ .WillByDefault(
+ Invoke([&](tGATT_IF client_if, const RawAddress& remote_bda,
+ bool is_direct, bool opportunistic) {
+ InjectConnectedEvent(remote_bda, global_conn_id++);
+ }));
+
+ ON_CALL(mock_gatt_interface_, Close(_))
+ .WillByDefault(Invoke(
+ [&](uint16_t conn_id) { InjectDisconnectedEvent(conn_id); }));
+
+ // default Characteristic read handler dispatches requests to service mocks
+ ON_CALL(mock_gatt_queue_, ReadCharacteristic(_, _, _, _))
+ .WillByDefault(Invoke([&](uint16_t conn_id, uint16_t handle,
+ GATT_READ_OP_CB cb, void* cb_data) {
+ do_in_main_thread(
+ FROM_HERE,
+ base::BindOnce(
+ [](std::map<uint16_t, std::unique_ptr<MockDeviceWrapper>>*
+ peer_devices,
+ uint16_t conn_id, uint16_t handle, GATT_READ_OP_CB cb,
+ void* cb_data) -> void {
+ if (peer_devices->count(conn_id)) {
+ auto& device = peer_devices->at(conn_id);
+ auto svc = std::find_if(
+ device->services.begin(), device->services.end(),
+ [handle](const gatt::Service& svc) {
+ return (handle >= svc.handle) &&
+ (handle <= svc.end_handle);
+ });
+ if (svc == device->services.end()) return;
+
+ // Dispatch to mockable handler functions
+ if (svc->handle == device->csis->start) {
+ device->csis->OnReadCharacteristic(handle, cb, cb_data);
+ } else if (svc->handle == device->cas->start) {
+ device->cas->OnReadCharacteristic(handle, cb, cb_data);
+ } else if (svc->handle == device->ascs->start) {
+ device->ascs->OnReadCharacteristic(handle, cb, cb_data);
+ } else if (svc->handle == device->pacs->start) {
+ device->pacs->OnReadCharacteristic(handle, cb, cb_data);
+ }
+ }
+ },
+ &peer_devices, conn_id, handle, cb, cb_data));
+ }));
+ }
+
+ void SetUpMockGroups() {
+ MockCsisClient::SetMockInstanceForTesting(&mock_csis_client_module_);
+ MockDeviceGroups::SetMockInstanceForTesting(&mock_groups_module_);
+ MockLeAudioGroupStateMachine::SetMockInstanceForTesting(
+ &mock_state_machine_);
+
+ ON_CALL(mock_csis_client_module_, Get())
+ .WillByDefault(Return(&mock_csis_client_module_));
+
+ // Store group callbacks so that we could inject grouping events
+ group_callbacks_ = nullptr;
+ ON_CALL(mock_groups_module_, Initialize(_))
+ .WillByDefault(SaveArg<0>(&group_callbacks_));
+
+ ON_CALL(mock_groups_module_, GetGroupId(_, _))
+ .WillByDefault([this](const RawAddress& addr, bluetooth::Uuid uuid) {
+ if (groups.find(addr) != groups.end()) return groups.at(addr);
+ return bluetooth::groups::kGroupUnknown;
+ });
+
+ ON_CALL(mock_groups_module_, RemoveDevice(_, _))
+ .WillByDefault([this](const RawAddress& addr, int group_id_) {
+ int group_id = -1;
+ if (groups.find(addr) != groups.end()) {
+ group_id = groups[addr];
+ groups.erase(addr);
+ }
+ if (group_id < 0) return;
+
+ do_in_main_thread(
+ FROM_HERE,
+ base::BindOnce(
+ [](const RawAddress& address, int group_id,
+ bluetooth::groups::DeviceGroupsCallbacks*
+ group_callbacks) {
+ group_callbacks->OnGroupMemberRemoved(address, group_id);
+ },
+ addr, group_id, base::Unretained(group_callbacks_)));
+ });
+
+ // Our test devices have unique LSB - use it for unique grouping when
+ // devices added with a non-CIS context and no grouping info
+ ON_CALL(mock_groups_module_,
+ AddDevice(_, le_audio::uuid::kCapServiceUuid, _))
+ .WillByDefault(
+ [this](const RawAddress& addr,
+ bluetooth::Uuid uuid = le_audio::uuid::kCapServiceUuid,
+ int group_id = bluetooth::groups::kGroupUnknown) -> int {
+ if (group_id == bluetooth::groups::kGroupUnknown) {
+ /* Generate group id from address */
+ groups[addr] = addr.address[RawAddress::kLength - 1];
+ group_id = groups[addr];
+ } else {
+ groups[addr] = group_id;
+ }
+
+ InjectGroupDeviceAdded(addr, groups[addr]);
+ return addr.address[RawAddress::kLength - 1];
+ });
+
+ ON_CALL(mock_state_machine_, Initialize(_))
+ .WillByDefault(SaveArg<0>(&state_machine_callbacks_));
+
+ ON_CALL(mock_state_machine_, StartStream(_, _))
+ .WillByDefault([this](LeAudioDeviceGroup* group,
+ types::LeAudioContextType context_type) {
+ if (group->GetState() ==
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ if (group->GetContextType() != context_type) {
+ /* TODO: Switch context of group */
+ group->SetContextType(context_type);
+ }
+ return true;
+ }
+
+ group->Configure(context_type);
+
+ // Fake ASE configuration
+ for (LeAudioDevice* device = group->GetFirstDevice();
+ device != nullptr; device = group->GetNextDevice(device)) {
+ for (auto& ase : device->ases_) {
+ if (!ase.active) continue;
+
+ // And also skip the ase establishment procedure which should
+ // be tested as part of the state machine unit tests
+ ase.data_path_state =
+ types::AudioStreamDataPathState::DATA_PATH_ESTABLISHED;
+ ase.cis_conn_hdl = iso_con_counter_++;
+ ase.active = true;
+ ase.state = types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING;
+ }
+ }
+
+ // Inject the state
+ group->SetTargetState(
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ group->SetState(group->GetTargetState());
+ state_machine_callbacks_->StatusReportCb(
+ group->group_id_, GroupStreamStatus::STREAMING);
+ streaming_groups[group->group_id_] = group;
+ return true;
+ });
+
+ ON_CALL(mock_state_machine_, SuspendStream(_))
+ .WillByDefault([this](LeAudioDeviceGroup* group) {
+ // Fake ASE state
+ for (LeAudioDevice* device = group->GetFirstDevice();
+ device != nullptr; device = group->GetNextDevice(device)) {
+ for (auto& ase : device->ases_) {
+ ase.data_path_state =
+ types::AudioStreamDataPathState::CIS_ESTABLISHED;
+ ase.active = false;
+ ase.state =
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED;
+ }
+ }
+
+ // Inject the state
+ group->SetTargetState(
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+ group->SetState(group->GetTargetState());
+ state_machine_callbacks_->StatusReportCb(
+ group->group_id_, GroupStreamStatus::SUSPENDED);
+ });
+
+ ON_CALL(mock_state_machine_, ProcessHciNotifAclDisconnected(_, _))
+ .WillByDefault([](LeAudioDeviceGroup* group,
+ LeAudioDevice* leAudioDevice) {
+ if (!group) return;
+ auto* stream_conf = &group->stream_conf;
+ if (stream_conf->valid) {
+ stream_conf->sink_streams.erase(
+ std::remove_if(stream_conf->sink_streams.begin(),
+ stream_conf->sink_streams.end(),
+ [leAudioDevice](auto& pair) {
+ auto ases = leAudioDevice->GetAsesByCisConnHdl(
+ pair.first);
+ return ases.sink;
+ }),
+ stream_conf->sink_streams.end());
+
+ stream_conf->source_streams.erase(
+ std::remove_if(stream_conf->source_streams.begin(),
+ stream_conf->source_streams.end(),
+ [leAudioDevice](auto& pair) {
+ auto ases = leAudioDevice->GetAsesByCisConnHdl(
+ pair.first);
+ return ases.source;
+ }),
+ stream_conf->source_streams.end());
+
+ if (stream_conf->sink_streams.empty()) {
+ LOG(INFO) << __func__ << " stream stopped ";
+ stream_conf->valid = false;
+ }
+ }
+ });
+
+ ON_CALL(mock_state_machine_, ProcessHciNotifCisDisconnected(_, _, _))
+ .WillByDefault(
+ [](LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice,
+ const bluetooth::hci::iso_manager::cis_disconnected_evt* event) {
+ if (!group) return;
+ auto ases_pair =
+ leAudioDevice->GetAsesByCisConnHdl(event->cis_conn_hdl);
+ if (ases_pair.sink) {
+ ases_pair.sink->data_path_state =
+ types::AudioStreamDataPathState::CIS_ASSIGNED;
+ }
+ if (ases_pair.source) {
+ ases_pair.source->data_path_state =
+ types::AudioStreamDataPathState::CIS_ASSIGNED;
+ }
+ /* Invalidate stream configuration if needed */
+ auto* stream_conf = &group->stream_conf;
+ if (stream_conf->valid) {
+ stream_conf->sink_streams.erase(
+ std::remove_if(
+ stream_conf->sink_streams.begin(),
+ stream_conf->sink_streams.end(),
+ [leAudioDevice](auto& pair) {
+ auto ases =
+ leAudioDevice->GetAsesByCisConnHdl(pair.first);
+ LOG(INFO) << __func__
+ << " sink ase to delete. Cis handle: "
+ << (int)(pair.first)
+ << " ase pointer: " << ases.sink;
+ return ases.sink;
+ }),
+ stream_conf->sink_streams.end());
+
+ stream_conf->source_streams.erase(
+ std::remove_if(
+ stream_conf->source_streams.begin(),
+ stream_conf->source_streams.end(),
+ [leAudioDevice](auto& pair) {
+ auto ases =
+ leAudioDevice->GetAsesByCisConnHdl(pair.first);
+ LOG(INFO)
+ << __func__ << " source to delete. Cis handle: "
+ << (int)(pair.first)
+ << " ase pointer: " << ases.source;
+ return ases.source;
+ }),
+ stream_conf->source_streams.end());
+
+ if (stream_conf->sink_streams.empty()) {
+ LOG(INFO) << __func__ << " stream stopped ";
+ stream_conf->valid = false;
+ }
+ }
+ });
+
+ ON_CALL(mock_state_machine_, StopStream(_))
+ .WillByDefault([this](LeAudioDeviceGroup* group) {
+ for (LeAudioDevice* device = group->GetFirstDevice();
+ device != nullptr; device = group->GetNextDevice(device)) {
+ for (auto& ase : device->ases_) {
+ ase.data_path_state = types::AudioStreamDataPathState::IDLE;
+ ase.active = false;
+ ase.state = types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE;
+ ase.cis_conn_hdl = 0;
+ }
+ }
+
+ // Inject the state
+ group->SetTargetState(types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+ group->SetState(group->GetTargetState());
+ state_machine_callbacks_->StatusReportCb(group->group_id_,
+ GroupStreamStatus::IDLE);
+ });
+ }
+
+ void SetUp() override {
+ init_message_loop_thread();
+
+ ON_CALL(controller_interface_, SupportsBleConnectedIsochronousStreamCentral)
+ .WillByDefault(Return(true));
+ ON_CALL(controller_interface_,
+ SupportsBleConnectedIsochronousStreamPeripheral)
+ .WillByDefault(Return(true));
+
+ controller::SetMockControllerInterface(&controller_interface_);
+ bluetooth::manager::SetMockBtmInterface(&mock_btm_interface_);
+ gatt::SetMockBtaGattInterface(&mock_gatt_interface_);
+ gatt::SetMockBtaGattQueue(&mock_gatt_queue_);
+ bluetooth::storage::SetMockBtifStorageInterface(&mock_btif_storage_);
+
+ iso_manager_ = bluetooth::hci::IsoManager::GetInstance();
+ ASSERT_NE(iso_manager_, nullptr);
+ iso_manager_->Start();
+
+ mock_iso_manager_ = MockIsoManager::GetInstance();
+ ON_CALL(*mock_iso_manager_, RegisterCigCallbacks(_))
+ .WillByDefault(SaveArg<0>(&cig_callbacks_));
+
+ SetUpMockAudioHal();
+ SetUpMockGroups();
+ SetUpMockGatt();
+
+ ASSERT_FALSE(LeAudioClient::IsLeAudioClientRunning());
+ }
+
+ void TearDown() override {
+ // Message loop cleanup should wait for all the 'till now' scheduled calls
+ // so it should be called right at the very begginning of teardown.
+ cleanup_message_loop_thread();
+
+ // This is required since Stop() and Cleanup() may trigger some callbacks or
+ // drop unique pointers to mocks we have raw pointer for and we want to
+ // verify them all.
+ Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
+
+ if (LeAudioClient::IsLeAudioClientRunning()) {
+ EXPECT_CALL(mock_gatt_interface_, AppDeregister(gatt_if)).Times(1);
+ LeAudioClient::Cleanup();
+ ASSERT_FALSE(LeAudioClient::IsLeAudioClientRunning());
+ }
+
+ iso_manager_->Stop();
+ }
+
+ protected:
+ class MockDeviceWrapper {
+ class IGattHandlers {
+ public:
+ // IGattHandlers() = default;
+ virtual ~IGattHandlers() = default;
+ virtual void OnReadCharacteristic(uint16_t handle, GATT_READ_OP_CB cb,
+ void* cb_data) = 0;
+ virtual void OnWriteCharacteristic(uint16_t handle,
+ std::vector<uint8_t> value,
+ tGATT_WRITE_TYPE write_type,
+ GATT_WRITE_OP_CB cb,
+ void* cb_data) = 0;
+ };
+
+ public:
+ struct csis_mock : public IGattHandlers {
+ uint16_t start = 0;
+ uint16_t end = 0;
+ uint16_t sirk_char = 0;
+ uint16_t sirk_ccc = 0;
+ uint16_t size_char = 0;
+ uint16_t size_ccc = 0;
+ uint16_t lock_char = 0;
+ uint16_t lock_ccc = 0;
+ uint16_t rank_char = 0;
+
+ int rank = 0;
+ int size = 0;
+
+ MOCK_METHOD((void), OnReadCharacteristic,
+ (uint16_t handle, GATT_READ_OP_CB cb, void* cb_data),
+ (override));
+ MOCK_METHOD((void), OnWriteCharacteristic,
+ (uint16_t handle, std::vector<uint8_t> value,
+ tGATT_WRITE_TYPE write_type, GATT_WRITE_OP_CB cb,
+ void* cb_data),
+ (override));
+ };
+
+ struct cas_mock : public IGattHandlers {
+ uint16_t start = 0;
+ uint16_t end = 0;
+ uint16_t csis_include = 0;
+
+ MOCK_METHOD((void), OnReadCharacteristic,
+ (uint16_t handle, GATT_READ_OP_CB cb, void* cb_data),
+ (override));
+ MOCK_METHOD((void), OnWriteCharacteristic,
+ (uint16_t handle, std::vector<uint8_t> value,
+ tGATT_WRITE_TYPE write_type, GATT_WRITE_OP_CB cb,
+ void* cb_data),
+ (override));
+ };
+
+ struct pacs_mock : public IGattHandlers {
+ uint16_t start = 0;
+ uint16_t sink_pac_char = 0;
+ uint16_t sink_pac_ccc = 0;
+ uint16_t sink_audio_loc_char = 0;
+ uint16_t sink_audio_loc_ccc = 0;
+ uint16_t source_pac_char = 0;
+ uint16_t source_pac_ccc = 0;
+ uint16_t source_audio_loc_char = 0;
+ uint16_t source_audio_loc_ccc = 0;
+ uint16_t avail_contexts_char = 0;
+ uint16_t avail_contexts_ccc = 0;
+ uint16_t supp_contexts_char = 0;
+ uint16_t supp_contexts_ccc = 0;
+ uint16_t end = 0;
+
+ MOCK_METHOD((void), OnReadCharacteristic,
+ (uint16_t handle, GATT_READ_OP_CB cb, void* cb_data),
+ (override));
+ MOCK_METHOD((void), OnWriteCharacteristic,
+ (uint16_t handle, std::vector<uint8_t> value,
+ tGATT_WRITE_TYPE write_type, GATT_WRITE_OP_CB cb,
+ void* cb_data),
+ (override));
+ };
+
+ struct ascs_mock : public IGattHandlers {
+ uint16_t start = 0;
+ uint16_t sink_ase_char = 0;
+ uint16_t sink_ase_ccc = 0;
+ uint16_t source_ase_char = 0;
+ uint16_t source_ase_ccc = 0;
+ uint16_t ctp_char = 0;
+ uint16_t ctp_ccc = 0;
+ uint16_t end = 0;
+
+ MOCK_METHOD((void), OnReadCharacteristic,
+ (uint16_t handle, GATT_READ_OP_CB cb, void* cb_data),
+ (override));
+ MOCK_METHOD((void), OnWriteCharacteristic,
+ (uint16_t handle, std::vector<uint8_t> value,
+ tGATT_WRITE_TYPE write_type, GATT_WRITE_OP_CB cb,
+ void* cb_data),
+ (override));
+ };
+
+ MockDeviceWrapper(RawAddress addr, const std::list<gatt::Service>& services,
+ std::unique_ptr<MockDeviceWrapper::csis_mock> csis,
+ std::unique_ptr<MockDeviceWrapper::cas_mock> cas,
+ std::unique_ptr<MockDeviceWrapper::ascs_mock> ascs,
+ std::unique_ptr<MockDeviceWrapper::pacs_mock> pacs)
+ : addr(addr) {
+ this->services = services;
+ this->csis = std::move(csis);
+ this->cas = std::move(cas);
+ this->ascs = std::move(ascs);
+ this->pacs = std::move(pacs);
+ }
+
+ ~MockDeviceWrapper() {
+ Mock::VerifyAndClearExpectations(csis.get());
+ Mock::VerifyAndClearExpectations(cas.get());
+ Mock::VerifyAndClearExpectations(ascs.get());
+ Mock::VerifyAndClearExpectations(pacs.get());
+ }
+
+ RawAddress addr;
+ bool connected = false;
+
+ // A list of services and their useful params
+ std::list<gatt::Service> services;
+ std::unique_ptr<csis_mock> csis;
+ std::unique_ptr<cas_mock> cas;
+ std::unique_ptr<ascs_mock> ascs;
+ std::unique_ptr<pacs_mock> pacs;
+ };
+
+ void SyncOnMainLoop() {
+ // Wait for the main loop to flush
+ // WARNING: Not tested with Timers pushing periodic tasks to the main loop
+ while (num_async_tasks > 0)
+ ;
+ }
+
+ void ConnectLeAudio(const RawAddress& address, bool isEncrypted = true) {
+ // by default indicate link as encrypted
+ ON_CALL(mock_btm_interface_, BTM_IsEncrypted(address, _))
+ .WillByDefault(DoAll(Return(isEncrypted)));
+
+ EXPECT_CALL(mock_gatt_interface_, Open(gatt_if, address, true, _)).Times(1);
+
+ do_in_main_thread(
+ FROM_HERE, base::Bind(&LeAudioClient::Connect,
+ base::Unretained(LeAudioClient::Get()), address));
+
+ SyncOnMainLoop();
+ }
+
+ void DisconnectLeAudio(const RawAddress& address, uint16_t conn_id) {
+ SyncOnMainLoop();
+ EXPECT_CALL(mock_gatt_interface_, Close(conn_id)).Times(1);
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::DISCONNECTED, address))
+ .Times(1);
+ do_in_main_thread(
+ FROM_HERE, base::Bind(&LeAudioClient::Disconnect,
+ base::Unretained(LeAudioClient::Get()), address));
+ }
+
+ void ConnectCsisDevice(const RawAddress& addr, uint16_t conn_id,
+ uint32_t sink_audio_allocation,
+ uint32_t source_audio_allocation, uint8_t group_size,
+ int group_id, uint8_t rank,
+ bool connect_through_csis = false) {
+ SetSampleDatabaseEarbudsValid(conn_id, addr, sink_audio_allocation,
+ source_audio_allocation, true, /*add_csis*/
+ true, /*add_cas*/
+ true, /*add_pacs*/
+ true, /*add_ascs*/
+ group_size, rank);
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::CONNECTED, addr))
+ .Times(1);
+
+ EXPECT_CALL(mock_client_callbacks_,
+ OnGroupNodeStatus(addr, group_id, GroupNodeStatus::ADDED))
+ .Times(1);
+
+ if (connect_through_csis) {
+ // Add it the way CSIS would do: add to group and then connect
+ do_in_main_thread(
+ FROM_HERE,
+ base::Bind(&LeAudioClient::GroupAddNode,
+ base::Unretained(LeAudioClient::Get()), group_id, addr));
+ ConnectLeAudio(addr);
+ } else {
+ // The usual connect
+ // Since device has CSIS, lets add it here to groups already now
+ groups[addr] = group_id;
+ ConnectLeAudio(addr);
+ InjectGroupDeviceAdded(addr, group_id);
+ }
+ }
+
+ void ConnectNonCsisDevice(const RawAddress& addr, uint16_t conn_id,
+ uint32_t sink_audio_allocation,
+ uint32_t source_audio_allocation) {
+ SetSampleDatabaseEarbudsValid(conn_id, addr, sink_audio_allocation,
+ source_audio_allocation, false, /*add_csis*/
+ true, /*add_cas*/
+ true, /*add_pacs*/
+ true, /*add_ascs*/
+ 0, 0);
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::CONNECTED, addr))
+ .Times(1);
+
+ ConnectLeAudio(addr);
+ }
+
+ void UpdateMetadata(audio_usage_t usage, audio_content_type_t content_type) {
+ std::promise<void> do_metadata_update_promise;
+ auto do_metadata_update_future = do_metadata_update_promise.get_future();
+ audio_sink_receiver_->OnAudioMetadataUpdate(
+ std::move(do_metadata_update_promise), usage, content_type);
+ do_metadata_update_future.wait();
+ }
+
+ void StartStreaming(audio_usage_t usage, audio_content_type_t content_type,
+ int group_id, bool reconfigured_sink = false) {
+ ASSERT_NE(audio_sink_receiver_, nullptr);
+
+ UpdateMetadata(usage, content_type);
+
+ EXPECT_CALL(mock_audio_source_, ConfirmStreamingRequest()).Times(1);
+ std::promise<void> do_resume_sink_promise;
+ auto do_resume_sink_future = do_resume_sink_promise.get_future();
+ /* It's enough to call only one suspend even if it'll be bi-directional
+ * streaming. First resume will trigger GroupStream.
+ *
+ * There is no - 'only source receiver' scenario (e.g. single microphone).
+ * If there will be such test oriented scenario, such resume choose logic
+ * should be applied.
+ */
+ audio_sink_receiver_->OnAudioResume(std::move(do_resume_sink_promise));
+ do_resume_sink_future.wait();
+ SyncOnMainLoop();
+
+ if (reconfigured_sink) {
+ std::promise<void> do_resume_sink_reconf_promise;
+ auto do_resume_sink_reconf_future =
+ do_resume_sink_reconf_promise.get_future();
+
+ audio_sink_receiver_->OnAudioResume(
+ std::move(do_resume_sink_reconf_promise));
+ do_resume_sink_reconf_future.wait();
+ }
+
+ if (usage == AUDIO_USAGE_VOICE_COMMUNICATION) {
+ ASSERT_NE(audio_source_receiver_, nullptr);
+
+ std::promise<void> do_resume_source_promise;
+ auto do_resume_source_future = do_resume_source_promise.get_future();
+ audio_source_receiver_->OnAudioResume(
+ std::move(do_resume_source_promise));
+ do_resume_source_future.wait();
+ }
+ }
+
+ void StopStreaming(int group_id, bool suspend_source = false) {
+ ASSERT_NE(audio_sink_receiver_, nullptr);
+
+ /* TODO We should have a way to confirm Stop() otherwise, audio framework
+ * might have different state that it is in the le_audio code - as tearing
+ * down CISes might take some time
+ */
+ std::promise<void> do_suspend_sink_promise;
+ auto do_suspend_sink_future = do_suspend_sink_promise.get_future();
+ /* It's enough to call only one resume even if it'll be bi-directional
+ * streaming. First suspend will trigger GroupStop.
+ *
+ * There is no - 'only source receiver' scenario (e.g. single microphone).
+ * If there will be such test oriented scenario, such resume choose logic
+ * should be applied.
+ */
+ audio_sink_receiver_->OnAudioSuspend(std::move(do_suspend_sink_promise));
+ do_suspend_sink_future.wait();
+
+ if (suspend_source) {
+ ASSERT_NE(audio_source_receiver_, nullptr);
+ std::promise<void> do_suspend_source_promise;
+ auto do_suspend_source_future = do_suspend_source_promise.get_future();
+ audio_source_receiver_->OnAudioSuspend(
+ std::move(do_suspend_source_promise));
+ do_suspend_source_future.wait();
+ }
+ }
+
+ void set_sample_database(uint16_t conn_id, RawAddress addr,
+ std::unique_ptr<MockDeviceWrapper::csis_mock> csis,
+ std::unique_ptr<MockDeviceWrapper::cas_mock> cas,
+ std::unique_ptr<MockDeviceWrapper::ascs_mock> ascs,
+ std::unique_ptr<MockDeviceWrapper::pacs_mock> pacs) {
+ gatt::DatabaseBuilder bob;
+
+ /* Generic Access Service */
+ bob.AddService(0x0001, 0x0003, Uuid::From16Bit(0x1800), true);
+ /* Device Name Char. */
+ bob.AddCharacteristic(0x0002, 0x0003, Uuid::From16Bit(0x2a00),
+ GATT_CHAR_PROP_BIT_READ);
+
+ if (csis->start) {
+ bool is_primary = true;
+ bob.AddService(csis->start, csis->end, bluetooth::csis::kCsisServiceUuid,
+ is_primary);
+ if (csis->sirk_char) {
+ bob.AddCharacteristic(
+ csis->sirk_char, csis->sirk_char + 1,
+ bluetooth::csis::kCsisSirkUuid,
+ GATT_CHAR_PROP_BIT_READ | GATT_CHAR_PROP_BIT_NOTIFY);
+ if (csis->sirk_ccc)
+ bob.AddDescriptor(csis->sirk_ccc,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ }
+
+ if (csis->size_char) {
+ bob.AddCharacteristic(
+ csis->size_char, csis->size_char + 1,
+ bluetooth::csis::kCsisSizeUuid,
+ GATT_CHAR_PROP_BIT_READ | GATT_CHAR_PROP_BIT_NOTIFY);
+ if (csis->size_ccc)
+ bob.AddDescriptor(csis->size_ccc,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ }
+
+ if (csis->lock_char) {
+ bob.AddCharacteristic(csis->lock_char, csis->lock_char + 1,
+ bluetooth::csis::kCsisLockUuid,
+ GATT_CHAR_PROP_BIT_READ |
+ GATT_CHAR_PROP_BIT_NOTIFY |
+ GATT_CHAR_PROP_BIT_WRITE);
+ if (csis->lock_ccc)
+ bob.AddDescriptor(csis->lock_ccc,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ }
+
+ if (csis->rank_char)
+ bob.AddCharacteristic(csis->rank_char, csis->rank_char + 1,
+ bluetooth::csis::kCsisRankUuid,
+ GATT_CHAR_PROP_BIT_READ);
+ }
+
+ if (cas->start) {
+ bool is_primary = true;
+ bob.AddService(cas->start, cas->end, le_audio::uuid::kCapServiceUuid,
+ is_primary);
+ // Include CSIS service inside
+ if (cas->csis_include)
+ bob.AddIncludedService(cas->csis_include,
+ bluetooth::csis::kCsisServiceUuid, csis->start,
+ csis->end);
+ }
+
+ if (pacs->start) {
+ bool is_primary = true;
+ bob.AddService(pacs->start, pacs->end,
+ le_audio::uuid::kPublishedAudioCapabilityServiceUuid,
+ is_primary);
+
+ if (pacs->sink_pac_char) {
+ bob.AddCharacteristic(
+ pacs->sink_pac_char, pacs->sink_pac_char + 1,
+ le_audio::uuid::kSinkPublishedAudioCapabilityCharacteristicUuid,
+ GATT_CHAR_PROP_BIT_READ);
+ if (pacs->sink_pac_ccc)
+ bob.AddDescriptor(pacs->sink_pac_ccc,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ }
+
+ if (pacs->sink_audio_loc_char) {
+ bob.AddCharacteristic(
+ pacs->sink_audio_loc_char, pacs->sink_audio_loc_char + 1,
+ le_audio::uuid::kSinkAudioLocationCharacteristicUuid,
+ GATT_CHAR_PROP_BIT_READ);
+ if (pacs->sink_audio_loc_ccc)
+ bob.AddDescriptor(pacs->sink_audio_loc_ccc,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ }
+
+ if (pacs->source_pac_char) {
+ bob.AddCharacteristic(
+ pacs->source_pac_char, pacs->source_pac_char + 1,
+ le_audio::uuid::kSourcePublishedAudioCapabilityCharacteristicUuid,
+ GATT_CHAR_PROP_BIT_READ);
+ if (pacs->source_pac_ccc)
+ bob.AddDescriptor(pacs->source_pac_ccc,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ }
+
+ if (pacs->source_audio_loc_char) {
+ bob.AddCharacteristic(
+ pacs->source_audio_loc_char, pacs->source_audio_loc_char + 1,
+ le_audio::uuid::kSourceAudioLocationCharacteristicUuid,
+ GATT_CHAR_PROP_BIT_READ);
+ if (pacs->source_audio_loc_ccc)
+ bob.AddDescriptor(pacs->source_audio_loc_ccc,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ }
+
+ if (pacs->avail_contexts_char) {
+ bob.AddCharacteristic(
+ pacs->avail_contexts_char, pacs->avail_contexts_char + 1,
+ le_audio::uuid::kAudioContextAvailabilityCharacteristicUuid,
+ GATT_CHAR_PROP_BIT_READ);
+ if (pacs->avail_contexts_ccc)
+ bob.AddDescriptor(pacs->avail_contexts_ccc,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ }
+
+ if (pacs->supp_contexts_char) {
+ bob.AddCharacteristic(
+ pacs->supp_contexts_char, pacs->supp_contexts_char + 1,
+ le_audio::uuid::kAudioSupportedContextCharacteristicUuid,
+ GATT_CHAR_PROP_BIT_READ);
+ if (pacs->supp_contexts_ccc)
+ bob.AddDescriptor(pacs->supp_contexts_ccc,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ }
+ }
+
+ if (ascs->start) {
+ bool is_primary = true;
+ bob.AddService(ascs->start, ascs->end,
+ le_audio::uuid::kAudioStreamControlServiceUuid,
+ is_primary);
+ if (ascs->sink_ase_char) {
+ bob.AddCharacteristic(ascs->sink_ase_char, ascs->sink_ase_char + 1,
+ le_audio::uuid::kSinkAudioStreamEndpointUuid,
+ GATT_CHAR_PROP_BIT_READ);
+ if (ascs->sink_ase_ccc)
+ bob.AddDescriptor(ascs->sink_ase_ccc,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ }
+ if (ascs->source_ase_char) {
+ bob.AddCharacteristic(ascs->source_ase_char, ascs->source_ase_char + 1,
+ le_audio::uuid::kSourceAudioStreamEndpointUuid,
+ GATT_CHAR_PROP_BIT_READ);
+ if (ascs->source_ase_ccc)
+ bob.AddDescriptor(ascs->source_ase_ccc,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ }
+ if (ascs->ctp_char) {
+ bob.AddCharacteristic(
+ ascs->ctp_char, ascs->ctp_char + 1,
+ le_audio::uuid::kAudioStreamEndpointControlPointCharacteristicUuid,
+ GATT_CHAR_PROP_BIT_READ);
+ if (ascs->ctp_ccc)
+ bob.AddDescriptor(ascs->ctp_ccc,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ }
+ }
+
+ // Assign conn_id to a certain device - this does not mean it is connected
+ auto dev_wrapper = std::make_unique<MockDeviceWrapper>(
+ addr, bob.Build().Services(), std::move(csis), std::move(cas),
+ std::move(ascs), std::move(pacs));
+ peer_devices.emplace(conn_id, std::move(dev_wrapper));
+ }
+
+ void SetSampleDatabaseEmpty(uint16_t conn_id, RawAddress addr) {
+ auto csis = std::make_unique<MockDeviceWrapper::csis_mock>();
+ auto cas = std::make_unique<MockDeviceWrapper::cas_mock>();
+ auto pacs = std::make_unique<MockDeviceWrapper::pacs_mock>();
+ auto ascs = std::make_unique<MockDeviceWrapper::ascs_mock>();
+ set_sample_database(conn_id, addr, std::move(csis), std::move(cas),
+ std::move(ascs), std::move(pacs));
+ }
+
+ void SetSampleDatabaseEarbudsValid(uint16_t conn_id, RawAddress addr,
+ uint32_t sink_audio_allocation,
+ uint32_t source_audio_allocation,
+ bool add_csis = true, bool add_cas = true,
+ bool add_pacs = true, bool add_ascs = true,
+ uint8_t set_size = 2, uint8_t rank = 1) {
+ auto csis = std::make_unique<MockDeviceWrapper::csis_mock>();
+ if (add_csis) {
+ // attribute handles
+ csis->start = 0x0010;
+ csis->sirk_char = 0x0020;
+ csis->sirk_ccc = 0x0022;
+ csis->size_char = 0x0023;
+ csis->size_ccc = 0x0025;
+ csis->lock_char = 0x0026;
+ csis->lock_ccc = 0x0028;
+ csis->rank_char = 0x0029;
+ csis->end = 0x0030;
+ // other params
+ csis->size = set_size;
+ csis->rank = rank;
+ }
+
+ auto cas = std::make_unique<MockDeviceWrapper::cas_mock>();
+ if (add_cas) {
+ // attribute handles
+ cas->start = 0x0040;
+ if (add_csis) cas->csis_include = 0x0041;
+ cas->end = 0x0050;
+ // other params
+ }
+
+ auto pacs = std::make_unique<MockDeviceWrapper::pacs_mock>();
+ if (add_pacs) {
+ // attribute handles
+ pacs->start = 0x0060;
+ pacs->sink_pac_char = 0x0061;
+ pacs->sink_pac_ccc = 0x0063;
+ pacs->sink_audio_loc_char = 0x0064;
+ pacs->sink_audio_loc_ccc = 0x0066;
+ pacs->source_pac_char = 0x0067;
+ pacs->source_pac_ccc = 0x0069;
+ pacs->source_audio_loc_char = 0x0070;
+ pacs->source_audio_loc_ccc = 0x0072;
+ pacs->avail_contexts_char = 0x0073;
+ pacs->avail_contexts_ccc = 0x0075;
+ pacs->supp_contexts_char = 0x0076;
+ pacs->supp_contexts_ccc = 0x0078;
+ pacs->end = 0x0080;
+ // other params
+ }
+
+ auto ascs = std::make_unique<MockDeviceWrapper::ascs_mock>();
+ if (add_ascs) {
+ // attribute handles
+ ascs->start = 0x0090;
+ ascs->sink_ase_char = 0x0091;
+ ascs->sink_ase_ccc = 0x0093;
+ ascs->source_ase_char = 0x0094;
+ ascs->source_ase_ccc = 0x0096;
+ ascs->ctp_char = 0x0097;
+ ascs->ctp_ccc = 0x0099;
+ ascs->end = 0x00A0;
+ // other params
+ }
+
+ set_sample_database(conn_id, addr, std::move(csis), std::move(cas),
+ std::move(ascs), std::move(pacs));
+
+ if (add_pacs) {
+ uint8_t snk_allocation[4];
+ uint8_t src_allocation[4];
+
+ snk_allocation[0] = (uint8_t)(sink_audio_allocation);
+ snk_allocation[1] = (uint8_t)(sink_audio_allocation >> 8);
+ snk_allocation[2] = (uint8_t)(sink_audio_allocation >> 16);
+ snk_allocation[3] = (uint8_t)(sink_audio_allocation >> 24);
+
+ src_allocation[0] = (uint8_t)(source_audio_allocation);
+ src_allocation[1] = (uint8_t)(source_audio_allocation >> 8);
+ src_allocation[2] = (uint8_t)(source_audio_allocation >> 16);
+ src_allocation[3] = (uint8_t)(source_audio_allocation >> 24);
+
+ // Set pacs default read values
+ ON_CALL(*peer_devices.at(conn_id)->pacs, OnReadCharacteristic(_, _, _))
+ .WillByDefault(
+ [this, conn_id, snk_allocation, src_allocation](
+ uint16_t handle, GATT_READ_OP_CB cb, void* cb_data) {
+ auto& pacs = peer_devices.at(conn_id)->pacs;
+ std::vector<uint8_t> value;
+ if (handle == pacs->sink_pac_char + 1) {
+ value = {
+ // Num records
+ 0x02,
+ // Codec_ID
+ 0x06,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ // Codec Spec. Caps. Len
+ 0x10,
+ 0x03,
+ 0x01,
+ 0x04,
+ 0x00,
+ 0x02,
+ 0x02,
+ 0x03,
+ 0x02,
+ 0x03,
+ 0x03,
+ 0x05,
+ 0x04,
+ 0x1E,
+ 0x00,
+ 0x28,
+ 0x00,
+ // Metadata Length
+ 0x00,
+ // Codec_ID
+ 0x06,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ // Codec Spec. Caps. Len
+ 0x10,
+ 0x03,
+ 0x01,
+ 0x80,
+ 0x00,
+ 0x02,
+ 0x02,
+ 0x03,
+ 0x02,
+ 0x03,
+ 0x03,
+ 0x05,
+ 0x04,
+ 0x78,
+ 0x00,
+ 0x78,
+ 0x00,
+ // Metadata Length
+ 0x00,
+ };
+ } else if (handle == pacs->sink_audio_loc_char + 1) {
+ value = {
+ // Audio Locations
+ snk_allocation[0],
+ snk_allocation[1],
+ snk_allocation[2],
+ snk_allocation[3],
+ };
+ } else if (handle == pacs->source_pac_char + 1) {
+ value = {
+ // Num records
+ 0x02,
+ // Codec_ID
+ 0x06,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ // Codec Spec. Caps. Len
+ 0x10,
+ 0x03,
+ 0x01,
+ 0x04,
+ 0x00,
+ 0x02,
+ 0x02,
+ 0x03,
+ 0x02,
+ 0x03,
+ 0x03,
+ 0x05,
+ 0x04,
+ 0x1E,
+ 0x00,
+ 0x28,
+ 0x00,
+ // Metadata Length
+ 0x00,
+ // Codec_ID
+ 0x06,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ // Codec Spec. Caps. Len
+ 0x10,
+ 0x03,
+ 0x01,
+ 0x04,
+ 0x00,
+ 0x02,
+ 0x02,
+ 0x03,
+ 0x02,
+ 0x03,
+ 0x03,
+ 0x05,
+ 0x04,
+ 0x1E,
+ 0x00,
+ 0x28,
+ 0x00,
+ // Metadata Length
+ 0x00,
+ };
+ } else if (handle == pacs->source_audio_loc_char + 1) {
+ value = {
+ // Audio Locations
+ src_allocation[0],
+ src_allocation[1],
+ src_allocation[2],
+ src_allocation[3],
+ };
+ } else if (handle == pacs->avail_contexts_char + 1) {
+ value = {
+ // Sink Avail Contexts
+ 0xff,
+ 0xff,
+ // Source Avail Contexts
+ 0xff,
+ 0xff,
+ };
+ } else if (handle == pacs->supp_contexts_char + 1) {
+ value = {
+ // Sink Avail Contexts
+ 0xff,
+ 0xff,
+ // Source Avail Contexts
+ 0xff,
+ 0xff,
+ };
+ }
+ cb(conn_id, GATT_SUCCESS, handle, value.size(), value.data(),
+ cb_data);
+ });
+ }
+
+ if (add_ascs) {
+ // Set ascs default read values
+ ON_CALL(*peer_devices.at(conn_id)->ascs, OnReadCharacteristic(_, _, _))
+ .WillByDefault([this, conn_id](uint16_t handle, GATT_READ_OP_CB cb,
+ void* cb_data) {
+ auto& ascs = peer_devices.at(conn_id)->ascs;
+ std::vector<uint8_t> value;
+ if (handle == ascs->sink_ase_char + 1) {
+ value = {
+ // ASE ID
+ 0x01,
+ // State
+ static_cast<uint8_t>(
+ le_audio::types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE),
+ // No Additional ASE params for IDLE state
+ };
+ } else if (handle == ascs->source_ase_char + 1) {
+ value = {
+ // ASE ID
+ 0x02,
+ // State
+ static_cast<uint8_t>(
+ le_audio::types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE),
+ // No Additional ASE params for IDLE state
+ };
+ }
+ cb(conn_id, GATT_SUCCESS, handle, value.size(), value.data(),
+ cb_data);
+ });
+ }
+ }
+
+ void TestAudioDataTransfer(int group_id, uint8_t cis_count_out,
+ uint8_t cis_count_in, int data_len) {
+ ASSERT_NE(audio_sink_receiver_, nullptr);
+
+ // Expect two channels ISO Data to be sent
+ std::vector<uint16_t> handles;
+ EXPECT_CALL(*mock_iso_manager_, SendIsoData(_, _, _))
+ .Times(cis_count_out)
+ .WillRepeatedly(
+ [&handles](uint16_t iso_handle, const uint8_t* data,
+ uint16_t data_len) { handles.push_back(iso_handle); });
+ std::vector<uint8_t> data(data_len);
+ audio_sink_receiver_->OnAudioDataReady(data);
+
+ // Inject microphone data from a single peer device
+ EXPECT_CALL(mock_audio_sink_, SendData(_, _)).Times(cis_count_in);
+ ASSERT_EQ(streaming_groups.count(group_id), 1u);
+
+ if (cis_count_in) {
+ ASSERT_NE(audio_source_receiver_, nullptr);
+
+ auto group = streaming_groups.at(group_id);
+ for (LeAudioDevice* device = group->GetFirstDevice(); device != nullptr;
+ device = group->GetNextDevice(device)) {
+ for (auto& ase : device->ases_) {
+ if (ase.direction == le_audio::types::kLeAudioDirectionSource) {
+ InjectIncomingIsoData(group_id, ase.cis_conn_hdl);
+ --cis_count_in;
+ if (!cis_count_in) break;
+ }
+ }
+ if (!cis_count_in) break;
+ }
+ }
+
+ SyncOnMainLoop();
+ std::sort(handles.begin(), handles.end());
+ ASSERT_EQ(std::unique(handles.begin(), handles.end()) - handles.begin(),
+ cis_count_out);
+ ASSERT_EQ(cis_count_in, 0);
+ handles.clear();
+
+ Mock::VerifyAndClearExpectations(mock_iso_manager_);
+ Mock::VerifyAndClearExpectations(&mock_audio_sink_);
+ }
+
+ void InjectIncomingIsoData(uint16_t cig_id, uint16_t cis_con_hdl,
+ size_t payload_size = 40) {
+ BT_HDR* bt_hdr = (BT_HDR*)malloc(sizeof(BT_HDR) + payload_size);
+
+ bt_hdr->offset = 0;
+ bt_hdr->len = payload_size;
+
+ bluetooth::hci::iso_manager::cis_data_evt cis_evt;
+ cis_evt.cig_id = cig_id;
+ cis_evt.cis_conn_hdl = cis_con_hdl;
+ cis_evt.ts = 0;
+ cis_evt.evt_lost = 0;
+ cis_evt.p_msg = bt_hdr;
+
+ ASSERT_NE(cig_callbacks_, nullptr);
+ cig_callbacks_->OnCisEvent(
+ bluetooth::hci::iso_manager::kIsoEventCisDataAvailable, &cis_evt);
+ free(bt_hdr);
+ }
+
+ void InjectCisDisconnected(uint16_t cig_id, uint16_t cis_con_hdl,
+ uint8_t reason = 0) {
+ bluetooth::hci::iso_manager::cis_disconnected_evt cis_evt;
+ cis_evt.cig_id = cig_id;
+ cis_evt.cis_conn_hdl = cis_con_hdl;
+ cis_evt.reason = reason;
+
+ ASSERT_NE(cig_callbacks_, nullptr);
+ cig_callbacks_->OnCisEvent(
+ bluetooth::hci::iso_manager::kIsoEventCisDisconnected, &cis_evt);
+ }
+
+ MockLeAudioClientCallbacks mock_client_callbacks_;
+ MockLeAudioClientAudioSource mock_audio_source_;
+ MockLeAudioClientAudioSink mock_audio_sink_;
+ LeAudioClientAudioSinkReceiver* audio_sink_receiver_ = nullptr;
+ LeAudioClientAudioSourceReceiver* audio_source_receiver_ = nullptr;
+
+ bool is_audio_hal_source_acquired;
+ bool is_audio_hal_sink_acquired;
+
+ MockCsisClient mock_csis_client_module_;
+ MockDeviceGroups mock_groups_module_;
+ bluetooth::groups::DeviceGroupsCallbacks* group_callbacks_;
+ MockLeAudioGroupStateMachine mock_state_machine_;
+
+ MockFunction<void()> mock_storage_load;
+ MockFunction<bool()> mock_hal_2_1_verifier;
+
+ controller::MockControllerInterface controller_interface_;
+ bluetooth::manager::MockBtmInterface mock_btm_interface_;
+ gatt::MockBtaGattInterface mock_gatt_interface_;
+ gatt::MockBtaGattQueue mock_gatt_queue_;
+ tBTA_GATTC_CBACK* gatt_callback;
+ const uint8_t gatt_if = 0xfe;
+ uint8_t global_conn_id = 1;
+ le_audio::LeAudioGroupStateMachine::Callbacks* state_machine_callbacks_;
+ std::map<int, LeAudioDeviceGroup*> streaming_groups;
+
+ bluetooth::hci::IsoManager* iso_manager_;
+ MockIsoManager* mock_iso_manager_;
+ bluetooth::hci::iso_manager::CigCallbacks* cig_callbacks_ = nullptr;
+ uint16_t iso_con_counter_ = 1;
+
+ bluetooth::storage::MockBtifStorageInterface mock_btif_storage_;
+
+ std::map<uint16_t, std::unique_ptr<MockDeviceWrapper>> peer_devices;
+ std::list<int> group_locks;
+ std::map<RawAddress, int> groups;
+};
+
+class UnicastTest : public UnicastTestNoInit {
+ protected:
+ void SetUp() override {
+ UnicastTestNoInit::SetUp();
+
+ EXPECT_CALL(mock_hal_2_1_verifier, Call()).Times(1);
+ EXPECT_CALL(mock_storage_load, Call()).Times(1);
+
+ BtaAppRegisterCallback app_register_callback;
+ EXPECT_CALL(mock_gatt_interface_, AppRegister(_, _, _))
+ .WillOnce(DoAll(SaveArg<0>(&gatt_callback),
+ SaveArg<1>(&app_register_callback)));
+ LeAudioClient::Initialize(
+ &mock_client_callbacks_,
+ base::Bind([](MockFunction<void()>* foo) { foo->Call(); },
+ &mock_storage_load),
+ base::Bind([](MockFunction<bool()>* foo) { return foo->Call(); },
+ &mock_hal_2_1_verifier));
+
+ SyncOnMainLoop();
+ ASSERT_TRUE(gatt_callback);
+ ASSERT_TRUE(group_callbacks_);
+ ASSERT_TRUE(app_register_callback);
+ app_register_callback.Run(gatt_if, GATT_SUCCESS);
+ Mock::VerifyAndClearExpectations(&mock_gatt_interface_);
+ }
+
+ void TearDown() override {
+ groups.clear();
+ UnicastTestNoInit::TearDown();
+ }
+};
+
+RawAddress GetTestAddress(uint8_t index) {
+ CHECK_LT(index, UINT8_MAX);
+ RawAddress result = {{0xC0, 0xDE, 0xC0, 0xDE, 0x00, index}};
+ return result;
+}
+
+TEST_F(UnicastTest, Initialize) {
+ ASSERT_NE(LeAudioClient::Get(), nullptr);
+ ASSERT_TRUE(LeAudioClient::IsLeAudioClientRunning());
+}
+
+TEST_F(UnicastTestNoInit, InitializeNoHal_2_1) {
+ ASSERT_FALSE(LeAudioClient::IsLeAudioClientRunning());
+
+ // Report False when asked for Audio HAL 2.1 support
+ ON_CALL(mock_hal_2_1_verifier, Call()).WillByDefault([]() -> bool {
+ return false;
+ });
+
+ BtaAppRegisterCallback app_register_callback;
+ ON_CALL(mock_gatt_interface_, AppRegister(_, _, _))
+ .WillByDefault(DoAll(SaveArg<0>(&gatt_callback),
+ SaveArg<1>(&app_register_callback)));
+
+ EXPECT_DEATH(
+ LeAudioClient::Initialize(
+ &mock_client_callbacks_,
+ base::Bind([](MockFunction<void()>* foo) { foo->Call(); },
+ &mock_storage_load),
+ base::Bind([](MockFunction<bool()>* foo) { return foo->Call(); },
+ &mock_hal_2_1_verifier)),
+ ", LE Audio Client requires Bluetooth Audio HAL V2.1 at least. Either "
+ "disable LE Audio Profile, or update your HAL");
+}
+
+TEST_F(UnicastTest, ConnectOneEarbudEmpty) {
+ const RawAddress test_address0 = GetTestAddress(0);
+ SetSampleDatabaseEmpty(1, test_address0);
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address0))
+ .Times(1);
+ EXPECT_CALL(mock_gatt_interface_, Close(_)).Times(1);
+ ConnectLeAudio(test_address0);
+}
+
+TEST_F(UnicastTest, ConnectOneEarbudNoPacs) {
+ const RawAddress test_address0 = GetTestAddress(0);
+ SetSampleDatabaseEarbudsValid(
+ 1, test_address0, codec_spec_conf::kLeAudioLocationStereo,
+ codec_spec_conf::kLeAudioLocationStereo, true, /*add_csis*/
+ true, /*add_cas*/
+ false, /*add_pacs*/
+ true /*add_ascs*/);
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address0))
+ .Times(1);
+ EXPECT_CALL(mock_gatt_interface_, Close(_)).Times(1);
+ ConnectLeAudio(test_address0);
+}
+
+TEST_F(UnicastTest, ConnectOneEarbudNoAscs) {
+ const RawAddress test_address0 = GetTestAddress(0);
+ SetSampleDatabaseEarbudsValid(
+ 1, test_address0, codec_spec_conf::kLeAudioLocationStereo,
+ codec_spec_conf::kLeAudioLocationStereo, true, /*add_csis*/
+ true, /*add_cas*/
+ true, /*add_pacs*/
+ false /*add_ascs*/);
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address0))
+ .Times(1);
+ EXPECT_CALL(mock_gatt_interface_, Close(_)).Times(1);
+ ConnectLeAudio(test_address0);
+}
+
+TEST_F(UnicastTest, ConnectOneEarbudNoCas) {
+ const RawAddress test_address0 = GetTestAddress(0);
+ uint16_t conn_id = 1;
+ SetSampleDatabaseEarbudsValid(
+ conn_id, test_address0, codec_spec_conf::kLeAudioLocationStereo,
+ codec_spec_conf::kLeAudioLocationStereo, true, /*add_csis*/
+ false, /*add_cas*/
+ true, /*add_pacs*/
+ true /*add_ascs*/);
+
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::CONNECTED, test_address0))
+ .Times(1);
+ ConnectLeAudio(test_address0);
+}
+
+TEST_F(UnicastTest, ConnectOneEarbudNoCsis) {
+ const RawAddress test_address0 = GetTestAddress(0);
+ SetSampleDatabaseEarbudsValid(
+ 1, test_address0, codec_spec_conf::kLeAudioLocationStereo,
+ codec_spec_conf::kLeAudioLocationStereo, false, /*add_csis*/
+ true, /*add_cas*/
+ true, /*add_pacs*/
+ true /*add_ascs*/);
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::CONNECTED, test_address0))
+ .Times(1);
+ ConnectLeAudio(test_address0);
+}
+
+TEST_F(UnicastTest, ConnectDisconnectOneEarbud) {
+ const RawAddress test_address0 = GetTestAddress(0);
+ SetSampleDatabaseEarbudsValid(1, test_address0,
+ codec_spec_conf::kLeAudioLocationStereo,
+ codec_spec_conf::kLeAudioLocationStereo);
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::CONNECTED, test_address0))
+ .Times(1);
+ ConnectLeAudio(test_address0);
+ DisconnectLeAudio(test_address0, 1);
+}
+
+TEST_F(UnicastTest, ConnectTwoEarbudsCsisGrouped) {
+ uint8_t group_size = 2;
+ int group_id = 2;
+
+ // Report working CSIS
+ ON_CALL(mock_csis_client_module_, IsCsisClientRunning())
+ .WillByDefault(Return(true));
+
+ // First earbud
+ const RawAddress test_address0 = GetTestAddress(0);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address0, true))
+ .Times(1);
+ ConnectCsisDevice(test_address0, 1 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontLeft,
+ codec_spec_conf::kLeAudioLocationFrontLeft, group_size,
+ group_id, 1 /* rank*/);
+
+ // Second earbud
+ const RawAddress test_address1 = GetTestAddress(1);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address1, true))
+ .Times(1);
+ ConnectCsisDevice(test_address1, 2 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontRight,
+ codec_spec_conf::kLeAudioLocationFrontRight, group_size,
+ group_id, 2 /* rank*/, true /*connect_through_csis*/);
+
+ // Verify grouping information
+ std::vector<RawAddress> devs =
+ LeAudioClient::Get()->GetGroupDevices(group_id);
+ ASSERT_NE(std::find(devs.begin(), devs.end(), test_address0), devs.end());
+ ASSERT_NE(std::find(devs.begin(), devs.end(), test_address1), devs.end());
+
+ DisconnectLeAudio(test_address0, 1);
+ DisconnectLeAudio(test_address1, 2);
+}
+
+TEST_F(UnicastTest, ConnectTwoEarbudsCsisGroupUnknownAtConnect) {
+ uint8_t group_size = 2;
+ uint8_t group_id = 2;
+
+ // Report working CSIS
+ ON_CALL(mock_csis_client_module_, IsCsisClientRunning())
+ .WillByDefault(Return(true));
+
+ // First earbud connects without known grouping
+ const RawAddress test_address0 = GetTestAddress(0);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address0, true))
+ .Times(1);
+ ConnectCsisDevice(test_address0, 1 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontLeft,
+ codec_spec_conf::kLeAudioLocationFrontLeft, group_size,
+ group_id, 1 /* rank*/);
+
+ // Second earbud
+ const RawAddress test_address1 = GetTestAddress(1);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address1, true))
+ .Times(1);
+ ConnectCsisDevice(test_address1, 2 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontRight,
+ codec_spec_conf::kLeAudioLocationFrontRight, group_size,
+ group_id, 2 /* rank*/, true /*connect_through_csis*/);
+
+ // Verify grouping information
+ std::vector<RawAddress> devs =
+ LeAudioClient::Get()->GetGroupDevices(group_id);
+ ASSERT_NE(std::find(devs.begin(), devs.end(), test_address0), devs.end());
+ ASSERT_NE(std::find(devs.begin(), devs.end(), test_address1), devs.end());
+
+ DisconnectLeAudio(test_address0, 1);
+ DisconnectLeAudio(test_address1, 2);
+}
+
+TEST_F(UnicastTestNoInit, LoadStoredEarbudsCsisGrouped) {
+ // Prepare two devices
+ uint8_t group_size = 2;
+ uint8_t group_id = 2;
+
+ const RawAddress test_address0 = GetTestAddress(0);
+ SetSampleDatabaseEarbudsValid(
+ 1, test_address0, codec_spec_conf::kLeAudioLocationFrontLeft,
+ codec_spec_conf::kLeAudioLocationFrontLeft, true, /*add_csis*/
+ true, /*add_cas*/
+ true, /*add_pacs*/
+ true, /*add_ascs*/
+ group_size, 1);
+
+ const RawAddress test_address1 = GetTestAddress(1);
+ SetSampleDatabaseEarbudsValid(
+ 2, test_address1, codec_spec_conf::kLeAudioLocationFrontRight,
+ codec_spec_conf::kLeAudioLocationFrontRight, true, /*add_csis*/
+ true, /*add_cas*/
+ true, /*add_pacs*/
+ true, /*add_ascs*/
+ group_size, 2);
+
+ // Load devices from the storage when storage API is called
+ bool autoconnect = true;
+ EXPECT_CALL(mock_storage_load, Call()).WillOnce([&]() {
+ do_in_main_thread(FROM_HERE, base::Bind(&LeAudioClient::AddFromStorage,
+ test_address0, autoconnect));
+ do_in_main_thread(FROM_HERE, base::Bind(&LeAudioClient::AddFromStorage,
+ test_address1, autoconnect));
+ });
+
+ // Expect stored device0 to connect automatically
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::CONNECTED, test_address0))
+ .Times(1);
+ ON_CALL(mock_btm_interface_, BTM_IsEncrypted(test_address0, _))
+ .WillByDefault(DoAll(Return(true)));
+ EXPECT_CALL(mock_gatt_interface_, Open(gatt_if, test_address0, true, _))
+ .Times(1);
+
+ // Expect stored device1 to connect automatically
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::CONNECTED, test_address1))
+ .Times(1);
+ ON_CALL(mock_btm_interface_, BTM_IsEncrypted(test_address1, _))
+ .WillByDefault(DoAll(Return(true)));
+ EXPECT_CALL(mock_gatt_interface_, Open(gatt_if, test_address1, true, _))
+ .Times(1);
+
+ ON_CALL(mock_groups_module_, GetGroupId(_, _))
+ .WillByDefault(DoAll(Return(group_id)));
+
+ ON_CALL(mock_btm_interface_,
+ GetSecurityFlagsByTransport(test_address0, NotNull(), _))
+ .WillByDefault(
+ DoAll(SetArgPointee<1>(BTM_SEC_FLAG_ENCRYPTED), Return(true)));
+
+ // Initialize
+ BtaAppRegisterCallback app_register_callback;
+ ON_CALL(mock_gatt_interface_, AppRegister(_, _, _))
+ .WillByDefault(DoAll(SaveArg<0>(&gatt_callback),
+ SaveArg<1>(&app_register_callback)));
+ LeAudioClient::Initialize(
+ &mock_client_callbacks_,
+ base::Bind([](MockFunction<void()>* foo) { foo->Call(); },
+ &mock_storage_load),
+ base::Bind([](MockFunction<bool()>* foo) { return foo->Call(); },
+ &mock_hal_2_1_verifier));
+ if (app_register_callback) app_register_callback.Run(gatt_if, GATT_SUCCESS);
+
+ // We need to wait for the storage callback before verifying stuff
+ SyncOnMainLoop();
+ ASSERT_TRUE(LeAudioClient::IsLeAudioClientRunning());
+
+ // Verify if all went well and we got the proper group
+ std::vector<RawAddress> devs =
+ LeAudioClient::Get()->GetGroupDevices(group_id);
+ ASSERT_NE(std::find(devs.begin(), devs.end(), test_address0), devs.end());
+ ASSERT_NE(std::find(devs.begin(), devs.end(), test_address1), devs.end());
+
+ DisconnectLeAudio(test_address0, 1);
+ DisconnectLeAudio(test_address1, 2);
+}
+
+TEST_F(UnicastTestNoInit, LoadStoredEarbudsCsisGroupedDifferently) {
+ // Prepare two devices
+ uint8_t group_size = 1;
+
+ // Device 0
+ uint8_t group_id0 = 2;
+ bool autoconnect0 = true;
+ const RawAddress test_address0 = GetTestAddress(0);
+ SetSampleDatabaseEarbudsValid(
+ 1, test_address0, codec_spec_conf::kLeAudioLocationFrontLeft,
+ codec_spec_conf::kLeAudioLocationFrontLeft, true, /*add_csis*/
+ true, /*add_cas*/
+ true, /*add_pacs*/
+ true, /*add_ascs*/
+ group_size, 1);
+
+ ON_CALL(mock_groups_module_, GetGroupId(test_address0, _))
+ .WillByDefault(DoAll(Return(group_id0)));
+
+ // Device 1
+ uint8_t group_id1 = 3;
+ bool autoconnect1 = false;
+ const RawAddress test_address1 = GetTestAddress(1);
+ SetSampleDatabaseEarbudsValid(
+ 2, test_address1, codec_spec_conf::kLeAudioLocationFrontRight,
+ codec_spec_conf::kLeAudioLocationFrontRight, true, /*add_csis*/
+ true, /*add_cas*/
+ true, /*add_pacs*/
+ true, /*add_ascs*/
+ group_size, 2);
+
+ ON_CALL(mock_groups_module_, GetGroupId(test_address1, _))
+ .WillByDefault(DoAll(Return(group_id1)));
+
+ // Load devices from the storage when storage API is called
+ EXPECT_CALL(mock_storage_load, Call()).WillOnce([&]() {
+ do_in_main_thread(FROM_HERE, base::Bind(&LeAudioClient::AddFromStorage,
+ test_address0, autoconnect0));
+ do_in_main_thread(FROM_HERE, base::Bind(&LeAudioClient::AddFromStorage,
+ test_address1, autoconnect1));
+ });
+
+ // Expect stored device0 to connect automatically
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::CONNECTED, test_address0))
+ .Times(1);
+ ON_CALL(mock_btm_interface_, BTM_IsEncrypted(test_address0, _))
+ .WillByDefault(DoAll(Return(true)));
+ EXPECT_CALL(mock_gatt_interface_, Open(gatt_if, test_address0, true, _))
+ .Times(1);
+
+ // Expect stored device1 to NOT connect automatically
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::CONNECTED, test_address1))
+ .Times(0);
+ ON_CALL(mock_btm_interface_, BTM_IsEncrypted(test_address1, _))
+ .WillByDefault(DoAll(Return(true)));
+ EXPECT_CALL(mock_gatt_interface_, Open(gatt_if, test_address1, true, _))
+ .Times(0);
+
+ // Initialize
+ BtaAppRegisterCallback app_register_callback;
+ ON_CALL(mock_gatt_interface_, AppRegister(_, _, _))
+ .WillByDefault(DoAll(SaveArg<0>(&gatt_callback),
+ SaveArg<1>(&app_register_callback)));
+ LeAudioClient::Initialize(
+ &mock_client_callbacks_,
+ base::Bind([](MockFunction<void()>* foo) { foo->Call(); },
+ &mock_storage_load),
+ base::Bind([](MockFunction<bool()>* foo) { return foo->Call(); },
+ &mock_hal_2_1_verifier));
+ if (app_register_callback) app_register_callback.Run(gatt_if, GATT_SUCCESS);
+
+ // We need to wait for the storage callback before verifying stuff
+ SyncOnMainLoop();
+ ASSERT_TRUE(LeAudioClient::IsLeAudioClientRunning());
+
+ std::vector<RawAddress> devs =
+ LeAudioClient::Get()->GetGroupDevices(group_id0);
+ ASSERT_NE(std::find(devs.begin(), devs.end(), test_address0), devs.end());
+ ASSERT_EQ(std::find(devs.begin(), devs.end(), test_address1), devs.end());
+
+ devs = LeAudioClient::Get()->GetGroupDevices(group_id1);
+ ASSERT_EQ(std::find(devs.begin(), devs.end(), test_address0), devs.end());
+ ASSERT_NE(std::find(devs.begin(), devs.end(), test_address1), devs.end());
+
+ DisconnectLeAudio(test_address0, 1);
+}
+
+TEST_F(UnicastTest, GroupingAddRemove) {
+ // Earbud connects without known grouping
+ uint8_t group_id0 = bluetooth::groups::kGroupUnknown;
+ const RawAddress test_address0 = GetTestAddress(0);
+
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address0, true))
+ .Times(1);
+ ConnectNonCsisDevice(test_address0, 1 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontLeft,
+ codec_spec_conf::kLeAudioLocationFrontLeft);
+
+ group_id0 = MockDeviceGroups::DeviceGroups::Get()->GetGroupId(test_address0);
+
+ // Earbud connects without known grouping
+ uint8_t group_id1 = bluetooth::groups::kGroupUnknown;
+ const RawAddress test_address1 = GetTestAddress(1);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address1, true))
+ .Times(1);
+ ConnectNonCsisDevice(test_address1, 2 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontRight,
+ codec_spec_conf::kLeAudioLocationFrontRight);
+
+ group_id1 = MockDeviceGroups::DeviceGroups::Get()->GetGroupId(test_address1);
+
+ Mock::VerifyAndClearExpectations(&mock_btif_storage_);
+
+ // Verify individual groups
+ ASSERT_NE(group_id0, bluetooth::groups::kGroupUnknown);
+ ASSERT_NE(group_id1, bluetooth::groups::kGroupUnknown);
+ ASSERT_NE(group_id0, group_id1);
+ ASSERT_EQ(LeAudioClient::Get()->GetGroupDevices(group_id0).size(), 1u);
+ ASSERT_EQ(LeAudioClient::Get()->GetGroupDevices(group_id1).size(), 1u);
+
+ // Expectations on reassigning second earbud to the first group
+ int dev1_storage_group = bluetooth::groups::kGroupUnknown;
+ int dev1_new_group = bluetooth::groups::kGroupUnknown;
+
+ EXPECT_CALL(
+ mock_client_callbacks_,
+ OnGroupNodeStatus(test_address1, group_id1, GroupNodeStatus::REMOVED))
+ .Times(AtLeast(1));
+ EXPECT_CALL(mock_client_callbacks_,
+ OnGroupNodeStatus(test_address1, _, GroupNodeStatus::ADDED))
+ .WillRepeatedly(SaveArg<1>(&dev1_new_group));
+ EXPECT_CALL(mock_groups_module_, RemoveDevice(test_address1, group_id1))
+ .Times(AtLeast(1));
+ EXPECT_CALL(mock_groups_module_, AddDevice(test_address1, _, _))
+ .Times(AnyNumber());
+
+ LeAudioClient::Get()->GroupRemoveNode(group_id1, test_address1);
+ SyncOnMainLoop();
+
+ Mock::VerifyAndClearExpectations(&mock_groups_module_);
+
+ EXPECT_CALL(mock_groups_module_, AddDevice(test_address1, _, group_id0))
+ .Times(1);
+
+ LeAudioClient::Get()->GroupAddNode(group_id0, test_address1);
+ SyncOnMainLoop();
+ Mock::VerifyAndClearExpectations(&mock_groups_module_);
+
+ dev1_storage_group =
+ MockDeviceGroups::DeviceGroups::Get()->GetGroupId(test_address1);
+
+ // Verify regrouping results
+ EXPECT_EQ(dev1_new_group, group_id0);
+ EXPECT_EQ(dev1_new_group, dev1_storage_group);
+ ASSERT_EQ(LeAudioClient::Get()->GetGroupDevices(group_id1).size(), 0u);
+ ASSERT_EQ(LeAudioClient::Get()->GetGroupDevices(group_id0).size(), 2u);
+ std::vector<RawAddress> devs =
+ LeAudioClient::Get()->GetGroupDevices(group_id0);
+ ASSERT_NE(std::find(devs.begin(), devs.end(), test_address0), devs.end());
+ ASSERT_NE(std::find(devs.begin(), devs.end(), test_address1), devs.end());
+}
+
+TEST_F(UnicastTest, GroupingAddTwiceNoRemove) {
+ // Earbud connects without known grouping
+ uint8_t group_id0 = bluetooth::groups::kGroupUnknown;
+ const RawAddress test_address0 = GetTestAddress(0);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address0, true))
+ .WillOnce(Return())
+ .RetiresOnSaturation();
+ ConnectNonCsisDevice(test_address0, 1 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontLeft,
+ codec_spec_conf::kLeAudioLocationFrontLeft);
+
+ group_id0 = MockDeviceGroups::DeviceGroups::Get()->GetGroupId(test_address0);
+
+ // Earbud connects without known grouping
+ uint8_t group_id1 = bluetooth::groups::kGroupUnknown;
+ const RawAddress test_address1 = GetTestAddress(1);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address1, true))
+ .WillOnce(Return())
+ .RetiresOnSaturation();
+ ConnectNonCsisDevice(test_address1, 2 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontRight,
+ codec_spec_conf::kLeAudioLocationFrontRight);
+
+ Mock::VerifyAndClearExpectations(&mock_btif_storage_);
+
+ group_id1 = MockDeviceGroups::DeviceGroups::Get()->GetGroupId(test_address1);
+ // Verify individual groups
+ ASSERT_NE(group_id0, bluetooth::groups::kGroupUnknown);
+ ASSERT_NE(group_id1, bluetooth::groups::kGroupUnknown);
+ ASSERT_NE(group_id0, group_id1);
+ ASSERT_EQ(LeAudioClient::Get()->GetGroupDevices(group_id0).size(), 1u);
+ ASSERT_EQ(LeAudioClient::Get()->GetGroupDevices(group_id1).size(), 1u);
+
+ // Expectations on reassigning second earbud to the first group
+ int dev1_storage_group = bluetooth::groups::kGroupUnknown;
+ int dev1_new_group = bluetooth::groups::kGroupUnknown;
+
+ EXPECT_CALL(
+ mock_client_callbacks_,
+ OnGroupNodeStatus(test_address1, group_id1, GroupNodeStatus::REMOVED))
+ .Times(AtLeast(1));
+ EXPECT_CALL(mock_client_callbacks_,
+ OnGroupNodeStatus(test_address1, _, GroupNodeStatus::ADDED))
+ .WillRepeatedly(SaveArg<1>(&dev1_new_group));
+
+ // FIXME: We should expect removal with group_id context. No such API exists.
+ EXPECT_CALL(mock_groups_module_, RemoveDevice(test_address1, group_id1))
+ .Times(AtLeast(1));
+ EXPECT_CALL(mock_groups_module_, AddDevice(test_address1, _, _))
+ .Times(AnyNumber());
+ EXPECT_CALL(mock_groups_module_, AddDevice(test_address1, _, group_id0))
+ .Times(1);
+
+ // Regroup device: assign new group without removing it from the first one
+ LeAudioClient::Get()->GroupAddNode(group_id0, test_address1);
+ SyncOnMainLoop();
+ Mock::VerifyAndClearExpectations(&mock_groups_module_);
+
+ dev1_storage_group =
+ MockDeviceGroups::DeviceGroups::Get()->GetGroupId(test_address1);
+
+ // Verify regrouping results
+ EXPECT_EQ(dev1_new_group, group_id0);
+ EXPECT_EQ(dev1_new_group, dev1_storage_group);
+ ASSERT_EQ(LeAudioClient::Get()->GetGroupDevices(group_id1).size(), 0u);
+ ASSERT_EQ(LeAudioClient::Get()->GetGroupDevices(group_id0).size(), 2u);
+ std::vector<RawAddress> devs =
+ LeAudioClient::Get()->GetGroupDevices(group_id0);
+ ASSERT_NE(std::find(devs.begin(), devs.end(), test_address0), devs.end());
+ ASSERT_NE(std::find(devs.begin(), devs.end(), test_address1), devs.end());
+}
+
+TEST_F(UnicastTest, RemoveTwoEarbudsCsisGrouped) {
+ uint8_t group_size = 2;
+ int group_id0 = 2;
+ int group_id1 = 3;
+
+ // Report working CSIS
+ ON_CALL(mock_csis_client_module_, IsCsisClientRunning())
+ .WillByDefault(Return(true));
+
+ // First group - First earbud
+ const RawAddress test_address0 = GetTestAddress(0);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address0, true))
+ .Times(1);
+ ConnectCsisDevice(test_address0, 1 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontLeft,
+ codec_spec_conf::kLeAudioLocationFrontLeft, group_size,
+ group_id0, 1 /* rank*/);
+
+ // First group - Second earbud
+ const RawAddress test_address1 = GetTestAddress(1);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address1, true))
+ .Times(1);
+ ConnectCsisDevice(test_address1, 2 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontRight,
+ codec_spec_conf::kLeAudioLocationFrontRight, group_size,
+ group_id0, 2 /* rank*/, true /*connect_through_csis*/);
+
+ // Second group - First earbud
+ const RawAddress test_address2 = GetTestAddress(2);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address2, true))
+ .Times(1);
+ ConnectCsisDevice(test_address2, 3 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontLeft,
+ codec_spec_conf::kLeAudioLocationFrontLeft, group_size,
+ group_id1, 1 /* rank*/);
+
+ // Second group - Second earbud
+ const RawAddress test_address3 = GetTestAddress(3);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address3, true))
+ .Times(1);
+ ConnectCsisDevice(test_address3, 4 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontRight,
+ codec_spec_conf::kLeAudioLocationFrontRight, group_size,
+ group_id1, 2 /* rank*/, true /*connect_through_csis*/);
+
+ // First group - verify grouping information
+ std::vector<RawAddress> group0_devs =
+ LeAudioClient::Get()->GetGroupDevices(group_id0);
+ ASSERT_NE(std::find(group0_devs.begin(), group0_devs.end(), test_address0),
+ group0_devs.end());
+ ASSERT_NE(std::find(group0_devs.begin(), group0_devs.end(), test_address1),
+ group0_devs.end());
+
+ // Second group - verify grouping information
+ std::vector<RawAddress> group1_devs =
+ LeAudioClient::Get()->GetGroupDevices(group_id1);
+ ASSERT_NE(std::find(group1_devs.begin(), group1_devs.end(), test_address2),
+ group1_devs.end());
+ ASSERT_NE(std::find(group1_devs.begin(), group1_devs.end(), test_address3),
+ group1_devs.end());
+ Mock::VerifyAndClearExpectations(&mock_btif_storage_);
+
+ // Expect one of the groups to be dropped and devices to be disconnected
+ EXPECT_CALL(mock_groups_module_, RemoveDevice(test_address0, group_id0))
+ .Times(1);
+ EXPECT_CALL(mock_groups_module_, RemoveDevice(test_address1, group_id0))
+ .Times(1);
+ EXPECT_CALL(
+ mock_client_callbacks_,
+ OnGroupNodeStatus(test_address0, group_id0, GroupNodeStatus::REMOVED));
+ EXPECT_CALL(
+ mock_client_callbacks_,
+ OnGroupNodeStatus(test_address1, group_id0, GroupNodeStatus::REMOVED));
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address0))
+ .Times(1);
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address1))
+ .Times(1);
+
+ // Expect the other groups to be left as is
+ EXPECT_CALL(mock_client_callbacks_, OnGroupStatus(group_id1, _)).Times(0);
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address2))
+ .Times(0);
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address3))
+ .Times(0);
+
+ do_in_main_thread(
+ FROM_HERE, base::Bind(&LeAudioClient::GroupDestroy,
+ base::Unretained(LeAudioClient::Get()), group_id0));
+
+ SyncOnMainLoop();
+ Mock::VerifyAndClearExpectations(&mock_btif_storage_);
+}
+
+TEST_F(UnicastTest, SpeakerStreaming) {
+ const RawAddress test_address0 = GetTestAddress(0);
+ int group_id = bluetooth::groups::kGroupUnknown;
+
+ SetSampleDatabaseEarbudsValid(
+ 1, test_address0, codec_spec_conf::kLeAudioLocationStereo,
+ codec_spec_conf::kLeAudioLocationStereo, false /*add_csis*/,
+ true /*add_cas*/, true /*add_pacs*/, true /*add_ascs*/, 1 /*set_size*/,
+ 0 /*rank*/);
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::CONNECTED, test_address0))
+ .Times(1);
+ EXPECT_CALL(mock_client_callbacks_,
+ OnGroupNodeStatus(test_address0, _, GroupNodeStatus::ADDED))
+ .WillOnce(DoAll(SaveArg<1>(&group_id)));
+
+ ConnectLeAudio(test_address0);
+ ASSERT_NE(group_id, bluetooth::groups::kGroupUnknown);
+
+ // Start streaming
+ uint8_t cis_count_out = 1;
+ uint8_t cis_count_in = 0;
+
+ EXPECT_CALL(mock_audio_source_, Start(_, _)).Times(1);
+ LeAudioClient::Get()->GroupSetActive(group_id);
+
+ StartStreaming(AUDIO_USAGE_MEDIA, AUDIO_CONTENT_TYPE_MUSIC, group_id);
+
+ Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+ SyncOnMainLoop();
+
+ // Verify Data transfer on one audio source cis
+ TestAudioDataTransfer(group_id, cis_count_out, cis_count_in, 1920);
+
+ // Suspend
+ /*TODO Need a way to verify STOP */
+ LeAudioClient::Get()->GroupSuspend(group_id);
+ Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+
+ // Resume
+ StartStreaming(AUDIO_USAGE_MEDIA, AUDIO_CONTENT_TYPE_MUSIC, group_id);
+ Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+
+ // Stop
+ StopStreaming(group_id);
+ Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
+
+ // Release
+ EXPECT_CALL(mock_audio_source_, Stop()).Times(1);
+ EXPECT_CALL(mock_audio_source_, Release(_)).Times(1);
+ LeAudioClient::Get()->GroupSetActive(bluetooth::groups::kGroupUnknown);
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+}
+
+TEST_F(UnicastTest, SpeakerStreamingAutonomousRelease) {
+ const RawAddress test_address0 = GetTestAddress(0);
+ int group_id = bluetooth::groups::kGroupUnknown;
+
+ SetSampleDatabaseEarbudsValid(
+ 1, test_address0, codec_spec_conf::kLeAudioLocationStereo,
+ codec_spec_conf::kLeAudioLocationStereo, false /*add_csis*/,
+ true /*add_cas*/, true /*add_pacs*/, true /*add_ascs*/, 1 /*set_size*/,
+ 0 /*rank*/);
+ EXPECT_CALL(mock_client_callbacks_,
+ OnConnectionState(ConnectionState::CONNECTED, test_address0))
+ .Times(1);
+ EXPECT_CALL(mock_client_callbacks_,
+ OnGroupNodeStatus(test_address0, _, GroupNodeStatus::ADDED))
+ .WillOnce(DoAll(SaveArg<1>(&group_id)));
+
+ ConnectLeAudio(test_address0);
+ ASSERT_NE(group_id, bluetooth::groups::kGroupUnknown);
+
+ // Start streaming
+ EXPECT_CALL(mock_audio_source_, Start(_, _)).Times(1);
+ LeAudioClient::Get()->GroupSetActive(group_id);
+
+ StartStreaming(AUDIO_USAGE_MEDIA, AUDIO_CONTENT_TYPE_MUSIC, group_id);
+
+ Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+ SyncOnMainLoop();
+
+ // Verify Data transfer on one audio source cis
+ TestAudioDataTransfer(group_id, 1 /* cis_count_out */, 0 /* cis_count_in */,
+ 1920);
+
+ // Inject the IDLE state as if an autonomous release happened
+ auto group = streaming_groups.at(group_id);
+ ASSERT_NE(group, nullptr);
+ for (LeAudioDevice* device = group->GetFirstDevice(); device != nullptr;
+ device = group->GetNextDevice(device)) {
+ for (auto& ase : device->ases_) {
+ ase.data_path_state = types::AudioStreamDataPathState::IDLE;
+ ase.state = types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE;
+ InjectCisDisconnected(group_id, ase.cis_conn_hdl);
+ }
+ }
+
+ // Verify no Data transfer after the autonomous release
+ TestAudioDataTransfer(group_id, 0 /* cis_count_out */, 0 /* cis_count_in */,
+ 1920);
+}
+
+TEST_F(UnicastTest, TwoEarbudsStreaming) {
+ uint8_t group_size = 2;
+ int group_id = 2;
+
+ // Report working CSIS
+ ON_CALL(mock_csis_client_module_, IsCsisClientRunning())
+ .WillByDefault(Return(true));
+
+ // First earbud
+ const RawAddress test_address0 = GetTestAddress(0);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address0, true))
+ .Times(1);
+ ConnectCsisDevice(test_address0, 1 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontLeft,
+ codec_spec_conf::kLeAudioLocationFrontLeft, group_size,
+ group_id, 1 /* rank*/);
+
+ // Second earbud
+ const RawAddress test_address1 = GetTestAddress(1);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address1, true))
+ .Times(1);
+ ConnectCsisDevice(test_address1, 2 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontRight,
+ codec_spec_conf::kLeAudioLocationFrontRight, group_size,
+ group_id, 2 /* rank*/, true /*connect_through_csis*/);
+
+ EXPECT_CALL(mock_audio_source_, Start(_, _)).Times(1);
+ LeAudioClient::Get()->GroupSetActive(group_id);
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+
+ // Start streaming with reconfiguration from default media stream setup
+ EXPECT_CALL(mock_audio_source_, Start(_, _)).Times(1);
+ EXPECT_CALL(mock_audio_source_, CancelStreamingRequest()).Times(1);
+ EXPECT_CALL(mock_audio_source_, Stop()).Times(1);
+ EXPECT_CALL(mock_audio_sink_, Start(_, _)).Times(1);
+
+ StartStreaming(AUDIO_USAGE_VOICE_COMMUNICATION, AUDIO_CONTENT_TYPE_SPEECH,
+ group_id, true /* reconfigure */);
+
+ Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+ Mock::VerifyAndClearExpectations(&mock_audio_sink_);
+ SyncOnMainLoop();
+
+ // Verify Data transfer on two peer sinks and one source
+ uint8_t cis_count_out = 2;
+ uint8_t cis_count_in = 1;
+ TestAudioDataTransfer(group_id, cis_count_out, cis_count_in, 640);
+
+ // Suspend
+ EXPECT_CALL(mock_audio_source_, Release(_)).Times(0);
+ EXPECT_CALL(mock_audio_sink_, Release(_)).Times(0);
+ EXPECT_CALL(mock_audio_source_, Stop()).Times(0);
+ EXPECT_CALL(mock_audio_sink_, Stop()).Times(0);
+ LeAudioClient::Get()->GroupSuspend(group_id);
+ SyncOnMainLoop();
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+ Mock::VerifyAndClearExpectations(&mock_audio_sink_);
+
+ // Resume
+ StartStreaming(AUDIO_USAGE_VOICE_COMMUNICATION, AUDIO_CONTENT_TYPE_SPEECH,
+ group_id);
+ SyncOnMainLoop();
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+ Mock::VerifyAndClearExpectations(&mock_audio_sink_);
+
+ // Verify Data transfer still works
+ TestAudioDataTransfer(group_id, cis_count_out, cis_count_in, 640);
+
+ // Stop
+ StopStreaming(group_id, true);
+ Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
+
+ // Release
+ EXPECT_CALL(mock_audio_source_, Stop()).Times(1);
+ EXPECT_CALL(mock_audio_source_, Release(_)).Times(1);
+ EXPECT_CALL(mock_audio_sink_, Stop()).Times(1);
+ EXPECT_CALL(mock_audio_sink_, Release(_)).Times(1);
+ LeAudioClient::Get()->GroupSetActive(bluetooth::groups::kGroupUnknown);
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+ Mock::VerifyAndClearExpectations(&mock_audio_sink_);
+}
+
+TEST_F(UnicastTest, TwoEarbudsStreamingContextSwitchSimple) {
+ uint8_t group_size = 2;
+ int group_id = 2;
+
+ // Report working CSIS
+ ON_CALL(mock_csis_client_module_, IsCsisClientRunning())
+ .WillByDefault(Return(true));
+
+ // First earbud
+ const RawAddress test_address0 = GetTestAddress(0);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address0, true))
+ .Times(1);
+ ConnectCsisDevice(test_address0, 1 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontLeft,
+ codec_spec_conf::kLeAudioLocationFrontLeft, group_size,
+ group_id, 1 /* rank*/);
+
+ // Second earbud
+ const RawAddress test_address1 = GetTestAddress(1);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address1, true))
+ .Times(1);
+ ConnectCsisDevice(test_address1, 2 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontRight,
+ codec_spec_conf::kLeAudioLocationFrontRight, group_size,
+ group_id, 2 /* rank*/, true /*connect_through_csis*/);
+
+ EXPECT_CALL(mock_audio_source_, Start(_, _)).Times(1);
+ LeAudioClient::Get()->GroupSetActive(group_id);
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+
+ // Start streaming with reconfiguration from default media stream setup
+ EXPECT_CALL(
+ mock_state_machine_,
+ StartStream(_, le_audio::types::LeAudioContextType::NOTIFICATIONS))
+ .Times(1);
+ EXPECT_CALL(mock_audio_source_, Start(_, _)).Times(1);
+ EXPECT_CALL(mock_audio_source_, CancelStreamingRequest()).Times(1);
+ EXPECT_CALL(mock_audio_source_, Stop()).Times(1);
+
+ StartStreaming(AUDIO_USAGE_NOTIFICATION, AUDIO_CONTENT_TYPE_UNKNOWN, group_id,
+ true /* reconfigure */);
+
+ Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+ SyncOnMainLoop();
+
+ // Do a content switch to ALERTS
+ EXPECT_CALL(mock_audio_source_, Release).Times(0);
+ EXPECT_CALL(mock_audio_source_, Stop).Times(0);
+ EXPECT_CALL(mock_audio_source_, Start).Times(0);
+ EXPECT_CALL(mock_state_machine_,
+ StartStream(_, le_audio::types::LeAudioContextType::ALERTS))
+ .Times(1);
+ UpdateMetadata(AUDIO_USAGE_ALARM, AUDIO_CONTENT_TYPE_UNKNOWN);
+ Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+
+ // Do a content switch to EMERGENCY
+ EXPECT_CALL(mock_audio_source_, Release).Times(0);
+ EXPECT_CALL(mock_audio_source_, Stop).Times(0);
+ EXPECT_CALL(mock_audio_source_, Start).Times(0);
+
+ EXPECT_CALL(
+ mock_state_machine_,
+ StartStream(_, le_audio::types::LeAudioContextType::EMERGENCYALARM))
+ .Times(1);
+ UpdateMetadata(AUDIO_USAGE_EMERGENCY, AUDIO_CONTENT_TYPE_UNKNOWN);
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+}
+
+TEST_F(UnicastTest, TwoEarbudsStreamingContextSwitchReconfigure) {
+ uint8_t group_size = 2;
+ int group_id = 2;
+
+ // Report working CSIS
+ ON_CALL(mock_csis_client_module_, IsCsisClientRunning())
+ .WillByDefault(Return(true));
+
+ // First earbud
+ const RawAddress test_address0 = GetTestAddress(0);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address0, true))
+ .Times(1);
+ ConnectCsisDevice(test_address0, 1 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontLeft,
+ codec_spec_conf::kLeAudioLocationFrontLeft, group_size,
+ group_id, 1 /* rank*/);
+
+ // Second earbud
+ const RawAddress test_address1 = GetTestAddress(1);
+ EXPECT_CALL(mock_btif_storage_, AddLeaudioAutoconnect(test_address1, true))
+ .Times(1);
+ ConnectCsisDevice(test_address1, 2 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontRight,
+ codec_spec_conf::kLeAudioLocationFrontRight, group_size,
+ group_id, 2 /* rank*/, true /*connect_through_csis*/);
+
+ // Start streaming MEDIA
+ EXPECT_CALL(mock_audio_source_, Start(_, _)).Times(1);
+ LeAudioClient::Get()->GroupSetActive(group_id);
+
+ StartStreaming(AUDIO_USAGE_MEDIA, AUDIO_CONTENT_TYPE_MUSIC, group_id);
+
+ Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+ SyncOnMainLoop();
+
+ // Verify Data transfer on two peer sinks
+ uint8_t cis_count_out = 2;
+ uint8_t cis_count_in = 0;
+ TestAudioDataTransfer(group_id, cis_count_out, cis_count_in, 1920);
+
+ // Stop
+ StopStreaming(group_id);
+ Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
+
+ // Start streaming
+ EXPECT_CALL(mock_audio_source_, Start(_, _)).Times(1);
+ EXPECT_CALL(mock_audio_source_, CancelStreamingRequest()).Times(1);
+ EXPECT_CALL(mock_audio_source_, Stop()).Times(1);
+ EXPECT_CALL(mock_audio_sink_, Start(_, _)).Times(1);
+ LeAudioClient::Get()->GroupSetActive(group_id);
+
+ StartStreaming(AUDIO_USAGE_VOICE_COMMUNICATION, AUDIO_CONTENT_TYPE_SPEECH,
+ group_id, true /* reconfigure */);
+
+ Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+ Mock::VerifyAndClearExpectations(&mock_audio_sink_);
+ SyncOnMainLoop();
+
+ // Verify Data transfer on two peer sinks and one source
+ cis_count_out = 2;
+ cis_count_in = 1;
+ TestAudioDataTransfer(group_id, cis_count_out, cis_count_in, 640);
+}
+
+TEST_F(UnicastTest, TwoEarbuds2ndLateConnect) {
+ uint8_t group_size = 2;
+ int group_id = 2;
+
+ // Report working CSIS
+ ON_CALL(mock_csis_client_module_, IsCsisClientRunning())
+ .WillByDefault(Return(true));
+
+ // First earbud
+ const RawAddress test_address0 = GetTestAddress(0);
+ ConnectCsisDevice(test_address0, 1 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontLeft,
+ codec_spec_conf::kLeAudioLocationFrontLeft, group_size,
+ group_id, 1 /* rank*/);
+
+ // Start streaming
+ EXPECT_CALL(mock_audio_source_, Start(_, _)).Times(1);
+ LeAudioClient::Get()->GroupSetActive(group_id);
+
+ StartStreaming(AUDIO_USAGE_MEDIA, AUDIO_CONTENT_TYPE_MUSIC, group_id);
+
+ Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+ SyncOnMainLoop();
+
+ // Expect one iso channel to be fed with data
+ uint8_t cis_count_out = 1;
+ uint8_t cis_count_in = 0;
+ TestAudioDataTransfer(group_id, cis_count_out, cis_count_in, 1920);
+
+ // Second earbud connects during stream
+ const RawAddress test_address1 = GetTestAddress(1);
+ ConnectCsisDevice(test_address1, 2 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontRight,
+ codec_spec_conf::kLeAudioLocationFrontRight, group_size,
+ group_id, 2 /* rank*/, true /*connect_through_csis*/);
+
+ /* We should expect two iso channels to be fed with data, but for now, when
+ * second device is connected later, we just continue stream to one device.
+ * TODO: improve it.
+ */
+ cis_count_out = 1;
+ cis_count_in = 0;
+ TestAudioDataTransfer(group_id, cis_count_out, cis_count_in, 1920);
+}
+
+TEST_F(UnicastTest, TwoEarbuds2ndDisconnect) {
+ uint8_t group_size = 2;
+ int group_id = 2;
+
+ // Report working CSIS
+ ON_CALL(mock_csis_client_module_, IsCsisClientRunning())
+ .WillByDefault(Return(true));
+
+ // First earbud
+ const RawAddress test_address0 = GetTestAddress(0);
+ ConnectCsisDevice(test_address0, 1 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontLeft,
+ codec_spec_conf::kLeAudioLocationFrontLeft, group_size,
+ group_id, 1 /* rank*/);
+
+ // Second earbud
+ const RawAddress test_address1 = GetTestAddress(1);
+ ConnectCsisDevice(test_address1, 2 /*conn_id*/,
+ codec_spec_conf::kLeAudioLocationFrontRight,
+ codec_spec_conf::kLeAudioLocationFrontRight, group_size,
+ group_id, 2 /* rank*/, true /*connect_through_csis*/);
+
+ // Start streaming
+ EXPECT_CALL(mock_audio_source_, Start(_, _)).Times(1);
+ LeAudioClient::Get()->GroupSetActive(group_id);
+
+ StartStreaming(AUDIO_USAGE_MEDIA, AUDIO_CONTENT_TYPE_MUSIC, group_id);
+
+ Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
+ Mock::VerifyAndClearExpectations(&mock_audio_source_);
+ SyncOnMainLoop();
+
+ // Expect two iso channels to be fed with data
+ uint8_t cis_count_out = 2;
+ uint8_t cis_count_in = 0;
+ TestAudioDataTransfer(group_id, cis_count_out, cis_count_in, 1920);
+
+ // Disconnect one device and expect the group to keep on streaming
+ EXPECT_CALL(mock_state_machine_, StopStream(_)).Times(0);
+ auto group = streaming_groups.at(group_id);
+ auto device = group->GetFirstDevice();
+ for (auto& ase : device->ases_) {
+ InjectCisDisconnected(group_id, ase.cis_conn_hdl);
+ }
+ DisconnectLeAudio(device->address_, 1);
+ SyncOnMainLoop();
+ Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
+
+ // Expect one channel ISO Data to be sent
+ cis_count_out = 1;
+ cis_count_in = 0;
+ TestAudioDataTransfer(group_id, cis_count_out, cis_count_in, 1920);
+}
+
+} // namespace
+} // namespace le_audio
diff --git a/bta/le_audio/le_audio_types.cc b/bta/le_audio/le_audio_types.cc
new file mode 100644
index 000000000..d8286e1ac
--- /dev/null
+++ b/bta/le_audio/le_audio_types.cc
@@ -0,0 +1,409 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com. Represented by EHIMA -
+ * www.ehima.com
+ *
+ * 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 contains definitions for Basic Audio Profile / Audio Stream Control
+ * and Published Audio Capabilities definitions, structures etc.
+ */
+
+#include "le_audio_types.h"
+
+#include <base/strings/string_number_conversions.h>
+
+#include "bt_types.h"
+#include "bta_api.h"
+#include "bta_le_audio_api.h"
+#include "client_audio.h"
+#include "client_parser.h"
+
+namespace le_audio {
+namespace set_configurations {
+using set_configurations::CodecCapabilitySetting;
+using types::acs_ac_record;
+using types::kLeAudioCodingFormatLC3;
+using types::kLeAudioDirectionSink;
+using types::kLeAudioDirectionSource;
+using types::LeAudioContextType;
+using types::LeAudioLc3Config;
+
+static uint8_t min_req_devices_cnt(
+ const AudioSetConfiguration* audio_set_conf) {
+ std::pair<uint8_t /* sink */, uint8_t /* source */> snk_src_pair(0, 0);
+
+ for (auto ent : (*audio_set_conf).confs) {
+ if (ent.direction == kLeAudioDirectionSink)
+ snk_src_pair.first += ent.device_cnt;
+ if (ent.direction == kLeAudioDirectionSource)
+ snk_src_pair.second += ent.device_cnt;
+ }
+
+ return std::max(snk_src_pair.first, snk_src_pair.second);
+}
+
+static uint8_t min_req_devices_cnt(
+ const AudioSetConfigurations* audio_set_confs) {
+ uint8_t curr_min_req_devices_cnt = 0xff;
+
+ for (auto ent : *audio_set_confs) {
+ uint8_t req_devices_cnt = min_req_devices_cnt(ent);
+ if (req_devices_cnt < curr_min_req_devices_cnt)
+ curr_min_req_devices_cnt = req_devices_cnt;
+ }
+
+ return curr_min_req_devices_cnt;
+}
+
+bool check_if_may_cover_scenario(const AudioSetConfigurations* audio_set_confs,
+ uint8_t group_size) {
+ if (!audio_set_confs) {
+ LOG(ERROR) << __func__ << ", no audio requirements for group";
+ return false;
+ }
+
+ return group_size >= min_req_devices_cnt(audio_set_confs);
+}
+
+bool check_if_may_cover_scenario(const AudioSetConfiguration* audio_set_conf,
+ uint8_t group_size) {
+ if (!audio_set_conf) {
+ LOG(ERROR) << __func__ << ", no audio requirement for group";
+ return false;
+ }
+
+ return group_size >= min_req_devices_cnt(audio_set_conf);
+}
+
+static bool IsCodecConfigurationSupported(const types::LeAudioLtvMap& pacs,
+ const LeAudioLc3Config& lc3_config) {
+ const auto& reqs = lc3_config.GetAsLtvMap();
+ uint8_t u8_req_val, u8_pac_val;
+ uint16_t u16_req_val, u16_pac_val;
+
+ /* Sampling frequency */
+ auto req = reqs.Find(codec_spec_conf::kLeAudioCodecLC3TypeSamplingFreq);
+ auto pac = pacs.Find(codec_spec_caps::kLeAudioCodecLC3TypeSamplingFreq);
+ if (!req || !pac) {
+ DLOG(ERROR) << __func__ << ", lack of sampling frequency fields";
+ return false;
+ }
+
+ u8_req_val = VEC_UINT8_TO_UINT8(req.value());
+ u16_pac_val = VEC_UINT8_TO_UINT16(pac.value());
+
+ /*
+ * Note: Requirements are in the codec configuration specification which
+ * are values coming from BAP Appendix A1.2.1
+ */
+ DLOG(INFO) << __func__ << " Req:SamplFreq=" << loghex(u8_req_val);
+ /* NOTE: Below is Codec specific cababilities comes form BAP Appendix A A1.1.1
+ * Note this is a bitfield
+ */
+ DLOG(INFO) << __func__ << " Pac:SamplFreq=" << loghex(u16_pac_val);
+
+ /* TODO: Integrate with codec capabilities */
+ if ((u8_req_val != codec_spec_conf::kLeAudioSamplingFreq16000Hz &&
+ u8_req_val != codec_spec_conf::kLeAudioSamplingFreq48000Hz) ||
+ !(u16_pac_val &
+ codec_spec_caps::SamplingFreqConfig2Capability(u8_req_val))) {
+ DLOG(ERROR) << __func__ << ", sampling frequency not supported";
+ return false;
+ }
+
+ /* Frame duration */
+ req = reqs.Find(codec_spec_conf::kLeAudioCodecLC3TypeFrameDuration);
+ pac = pacs.Find(codec_spec_caps::kLeAudioCodecLC3TypeFrameDuration);
+ if (!req || !pac) {
+ DLOG(ERROR) << __func__ << ", lack of frame duration fields";
+ return false;
+ }
+
+ u8_req_val = VEC_UINT8_TO_UINT8(req.value());
+ u8_pac_val = VEC_UINT8_TO_UINT8(pac.value());
+ DLOG(INFO) << __func__ << " Req:FrameDur=" << loghex(u8_req_val);
+ DLOG(INFO) << __func__ << " Pac:FrameDur=" << loghex(u8_pac_val);
+
+ if ((u8_req_val != codec_spec_conf::kLeAudioCodecLC3FrameDur7500us &&
+ u8_req_val != codec_spec_conf::kLeAudioCodecLC3FrameDur10000us) ||
+ !(u8_pac_val &
+ (codec_spec_caps::FrameDurationConfig2Capability(u8_req_val)))) {
+ DLOG(ERROR) << __func__ << ", frame duration not supported";
+ return false;
+ }
+
+ uint8_t required_audio_chan_num = lc3_config.GetChannelCount();
+ pac = pacs.Find(codec_spec_caps::kLeAudioCodecLC3TypeAudioChannelCounts);
+
+ /*
+ * BAP_Validation_r07 1.9.2 Audio channel support requirements
+ * "The Unicast Server shall support an Audio_Channel_Counts value of 0x01
+ * (0b00000001 = one channel) and may support other values defined by an
+ * implementation or by a higher-layer specification."
+ *
+ * Thus if Audio_Channel_Counts is not present in PAC LTV structure, we assume
+ * the Unicast Server supports mandatory one channel.
+ */
+ if (!pac) {
+ DLOG(WARNING) << __func__ << ", no Audio_Channel_Counts field in PAC";
+ u8_pac_val = 0x01;
+ } else {
+ u8_pac_val = VEC_UINT8_TO_UINT8(pac.value());
+ }
+
+ DLOG(INFO) << __func__ << " Pac:AudioChanCnt=" << loghex(u8_pac_val);
+ if (!((1 << (required_audio_chan_num - 1)) & u8_pac_val)) {
+ DLOG(ERROR) << __func__ << ", channel count warning";
+ return false;
+ }
+
+ /* Octets per frame */
+ req = reqs.Find(codec_spec_conf::kLeAudioCodecLC3TypeOctetPerFrame);
+ pac = pacs.Find(codec_spec_caps::kLeAudioCodecLC3TypeOctetPerFrame);
+
+ if (!req || !pac) {
+ DLOG(ERROR) << __func__ << ", lack of octet per frame fields";
+ return false;
+ }
+
+ u16_req_val = VEC_UINT8_TO_UINT16(req.value());
+ DLOG(INFO) << __func__ << " Req:OctetsPerFrame=" << int(u16_req_val);
+
+ /* Minimal value 0-1 byte */
+ u16_pac_val = VEC_UINT8_TO_UINT16(pac.value());
+ DLOG(INFO) << __func__ << " Pac:MinOctetsPerFrame=" << int(u16_pac_val);
+ if (u16_req_val < u16_pac_val) {
+ DLOG(ERROR) << __func__ << ", octet per frame below minimum";
+ return false;
+ }
+
+ /* Maximal value 2-3 byte */
+ u16_pac_val = OFF_VEC_UINT8_TO_UINT16(pac.value(), 2);
+ DLOG(INFO) << __func__ << " Pac:MaxOctetsPerFrame=" << int(u16_pac_val);
+ if (u16_req_val > u16_pac_val) {
+ DLOG(ERROR) << __func__ << ", octet per frame above maximum";
+ return false;
+ }
+
+ return true;
+}
+
+bool IsCodecCapabilitySettingSupported(
+ const acs_ac_record& pac,
+ const CodecCapabilitySetting& codec_capability_setting) {
+ const auto& codec_id = codec_capability_setting.id;
+
+ if (codec_id != pac.codec_id) return false;
+
+ DLOG(INFO) << __func__ << ": Settings for format " << +codec_id.coding_format;
+
+ switch (codec_id.coding_format) {
+ case kLeAudioCodingFormatLC3:
+ return IsCodecConfigurationSupported(
+ pac.codec_spec_caps,
+ std::get<LeAudioLc3Config>(codec_capability_setting.config));
+ default:
+ return false;
+ }
+}
+
+const AudioSetConfigurations* get_confs_by_type(LeAudioContextType type) {
+ switch (type) {
+ case LeAudioContextType::MEDIA:
+ return &audio_set_conf_media;
+ case LeAudioContextType::CONVERSATIONAL:
+ return &audio_set_conf_conversational;
+ case LeAudioContextType::RINGTONE:
+ return &audio_set_conf_ringtone;
+ default:
+ return &audio_set_conf_default;
+ }
+}
+uint32_t CodecCapabilitySetting::GetConfigSamplingFrequency() const {
+ switch (id.coding_format) {
+ case kLeAudioCodingFormatLC3:
+ return std::get<types::LeAudioLc3Config>(config).GetSamplingFrequencyHz();
+ default:
+ DLOG(WARNING) << __func__ << ", invalid codec id";
+ return 0;
+ }
+};
+
+uint32_t CodecCapabilitySetting::GetConfigDataIntervalUs() const {
+ switch (id.coding_format) {
+ case kLeAudioCodingFormatLC3:
+ return std::get<types::LeAudioLc3Config>(config).GetFrameDurationUs();
+ default:
+ DLOG(WARNING) << __func__ << ", invalid codec id";
+ return 0;
+ }
+};
+
+uint8_t CodecCapabilitySetting::GetConfigBitsPerSample() const {
+ switch (id.coding_format) {
+ case kLeAudioCodingFormatLC3:
+ /* XXX LC3 supports 16, 24, 32 */
+ return 16;
+ default:
+ DLOG(WARNING) << __func__ << ", invalid codec id";
+ return 0;
+ }
+};
+
+uint8_t CodecCapabilitySetting::GetConfigChannelCount() const {
+ switch (id.coding_format) {
+ case kLeAudioCodingFormatLC3:
+ DLOG(INFO) << __func__ << ", count = "
+ << static_cast<int>(std::get<types::LeAudioLc3Config>(config)
+ .channel_count);
+ return std::get<types::LeAudioLc3Config>(config).channel_count;
+ default:
+ DLOG(WARNING) << __func__ << ", invalid codec id";
+ return 0;
+ }
+}
+} // namespace set_configurations
+
+namespace types {
+/* Helper map for matching various frequency notations */
+const std::map<uint8_t, uint32_t> LeAudioLc3Config::sampling_freq_map = {
+ {codec_spec_conf::kLeAudioSamplingFreq8000Hz,
+ LeAudioCodecConfiguration::kSampleRate8000},
+ {codec_spec_conf::kLeAudioSamplingFreq16000Hz,
+ LeAudioCodecConfiguration::kSampleRate16000},
+ {codec_spec_conf::kLeAudioSamplingFreq24000Hz,
+ LeAudioCodecConfiguration::kSampleRate24000},
+ {codec_spec_conf::kLeAudioSamplingFreq32000Hz,
+ LeAudioCodecConfiguration::kSampleRate32000},
+ {codec_spec_conf::kLeAudioSamplingFreq44100Hz,
+ LeAudioCodecConfiguration::kSampleRate44100},
+ {codec_spec_conf::kLeAudioSamplingFreq48000Hz,
+ LeAudioCodecConfiguration::kSampleRate48000}};
+
+/* Helper map for matching various frame durations notations */
+const std::map<uint8_t, uint32_t> LeAudioLc3Config::frame_duration_map = {
+ {codec_spec_conf::kLeAudioCodecLC3FrameDur7500us,
+ LeAudioCodecConfiguration::kInterval7500Us},
+ {codec_spec_conf::kLeAudioCodecLC3FrameDur10000us,
+ LeAudioCodecConfiguration::kInterval10000Us}};
+
+std::optional<std::vector<uint8_t>> LeAudioLtvMap::Find(uint8_t type) const {
+ auto iter =
+ std::find_if(values.cbegin(), values.cend(),
+ [type](const auto& value) { return value.first == type; });
+
+ if (iter == values.cend()) return std::nullopt;
+
+ return iter->second;
+}
+
+uint8_t* LeAudioLtvMap::RawPacket(uint8_t* p_buf) const {
+ for (auto const& value : values) {
+ UINT8_TO_STREAM(p_buf, value.second.size() + 1);
+ UINT8_TO_STREAM(p_buf, value.first);
+ ARRAY_TO_STREAM(p_buf, value.second.data(),
+ static_cast<int>(value.second.size()));
+ }
+
+ return p_buf;
+}
+
+LeAudioLtvMap LeAudioLtvMap::Parse(const uint8_t* p_value, uint8_t len,
+ bool& success) {
+ LeAudioLtvMap ltv_map;
+
+ if (len > 0) {
+ const auto p_value_end = p_value + len;
+
+ while ((p_value_end - p_value) > 0) {
+ uint8_t ltv_len;
+ STREAM_TO_UINT8(ltv_len, p_value);
+
+ // Unusual, but possible case
+ if (ltv_len == 0) continue;
+
+ if (p_value_end < (p_value + ltv_len)) {
+ LOG(ERROR) << __func__
+ << " Invalid ltv_len: " << static_cast<int>(ltv_len);
+ success = false;
+ return LeAudioLtvMap();
+ }
+
+ uint8_t ltv_type;
+ STREAM_TO_UINT8(ltv_type, p_value);
+ ltv_len -= sizeof(ltv_type);
+
+ const auto p_temp = p_value;
+ p_value += ltv_len;
+
+ std::vector<uint8_t> ltv_value(p_temp, p_value);
+ ltv_map.values.emplace(ltv_type, std::move(ltv_value));
+ }
+ }
+
+ success = true;
+ return ltv_map;
+}
+
+size_t LeAudioLtvMap::RawPacketSize() const {
+ size_t bytes = 0;
+
+ for (auto const& value : values) {
+ bytes += (/* ltv_len + ltv_type */ 2 + value.second.size());
+ }
+
+ return bytes;
+}
+
+std::string LeAudioLtvMap::ToString() const {
+ std::string debug_str;
+
+ for (const auto& value : values) {
+ std::stringstream sstream;
+
+ sstream << "\ttype: " << std::to_string(value.first)
+ << "\tlen: " << std::to_string(value.second.size()) << "\tdata: "
+ << base::HexEncode(value.second.data(), value.second.size()) + "\n";
+
+ debug_str += sstream.str();
+ }
+
+ return debug_str;
+}
+
+} // namespace types
+} // namespace le_audio
+
+std::ostream& operator<<(std::ostream& os,
+ const le_audio::types::LeAudioLc3Config& config) {
+ os << " LeAudioLc3Config(SamplFreq=" << loghex(config.sampling_frequency)
+ << ", FrameDur=" << loghex(config.frame_duration)
+ << ", OctetsPerFrame=" << int(config.octets_per_codec_frame)
+ << ", AudioChanLoc=" << loghex(config.audio_channel_allocation) << ")";
+ return os;
+}
+
+std::ostream& operator<<(std::ostream& os,
+ const le_audio::types::AseState& state) {
+ static const char* char_value_[7] = {
+ "IDLE", "CODEC_CONFIGURED", "QOS_CONFIGURED", "ENABLING",
+ "STREAMING", "DISABLING", "RELEASING",
+ };
+
+ os << char_value_[static_cast<uint8_t>(state)] << " ("
+ << "0x" << std::setfill('0') << std::setw(2) << static_cast<int>(state)
+ << ")";
+ return os;
+}
diff --git a/bta/le_audio/le_audio_types.h b/bta/le_audio/le_audio_types.h
new file mode 100644
index 000000000..ec0e2e3bc
--- /dev/null
+++ b/bta/le_audio/le_audio_types.h
@@ -0,0 +1,952 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com. Represented by EHIMA -
+ * www.ehima.com
+ *
+ * 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 contains definitions for Basic Audio Profile / Audio Stream Control
+ * and Published Audio Capabilities definitions, structures etc.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <map>
+#include <optional>
+#include <string>
+#include <variant>
+#include <vector>
+
+#include "bta_groups.h"
+#include "bta_le_audio_api.h"
+#include "btm_iso_api_types.h"
+
+namespace le_audio {
+
+#define UINT8_TO_VEC_UINT8(u8) \
+ std::vector<uint8_t> { u8 }
+#define UINT16_TO_VEC_UINT8(u16) \
+ std::vector<uint8_t>((uint8_t*)&u16, (uint8_t*)&u16 + sizeof(u16))
+#define UINT32_TO_VEC_UINT8(u32) \
+ std::vector<uint8_t>((uint8_t*)&u32, (uint8_t*)&u32 + sizeof(u32))
+
+#define VEC_UINT8_TO_UINT8(vec) vec.data()[0]
+#define VEC_UINT8_TO_UINT16(vec) ((vec.data()[1] << 8) + vec.data()[0])
+#define OFF_VEC_UINT8_TO_UINT16(vec, off) \
+ ((vec.data()[1 + off] << 8) + vec.data()[0 + off])
+#define VEC_UINT8_TO_UINT32(vec) \
+ ((vec.data()[3] << 24) + (vec.data()[2] << 16) + (vec.data()[1] << 8) + \
+ vec.data()[0])
+
+namespace uuid {
+/* CAP service
+ * This service is used to identify peer role (which we are not using for now)
+ * and to wrap CSIS service as this is required to understand the context of the
+ * CSIS
+ * Place holder
+ */
+static const bluetooth::Uuid kCapServiceUuid =
+ bluetooth::Uuid::From16Bit(0xEEEE);
+
+/* Assigned numbers for attributes */
+static const bluetooth::Uuid kPublishedAudioCapabilityServiceUuid =
+ bluetooth::Uuid::From16Bit(0x1850);
+static const bluetooth::Uuid kAudioStreamControlServiceUuid =
+ bluetooth::Uuid::From16Bit(0x184E);
+
+/* Published Audio Capabilities Service Characteristics */
+static const bluetooth::Uuid kSinkPublishedAudioCapabilityCharacteristicUuid =
+ bluetooth::Uuid::From16Bit(0x2BC9);
+static const bluetooth::Uuid kSourcePublishedAudioCapabilityCharacteristicUuid =
+ bluetooth::Uuid::From16Bit(0x2BCB);
+static const bluetooth::Uuid kSinkAudioLocationCharacteristicUuid =
+ bluetooth::Uuid::From16Bit(0x2BCA);
+static const bluetooth::Uuid kSourceAudioLocationCharacteristicUuid =
+ bluetooth::Uuid::From16Bit(0x2BCC);
+
+/* Audio Stream Control Service Characteristics */
+static const bluetooth::Uuid kAudioContextAvailabilityCharacteristicUuid =
+ bluetooth::Uuid::From16Bit(0x2BCD);
+static const bluetooth::Uuid kAudioSupportedContextCharacteristicUuid =
+ bluetooth::Uuid::From16Bit(0x2BCE);
+
+/* Audio Stream Control Service Characteristics */
+static const bluetooth::Uuid kSinkAudioStreamEndpointUuid =
+ bluetooth::Uuid::From16Bit(0x2BC4);
+static const bluetooth::Uuid kSourceAudioStreamEndpointUuid =
+ bluetooth::Uuid::From16Bit(0x2BC5);
+static const bluetooth::Uuid
+ kAudioStreamEndpointControlPointCharacteristicUuid =
+ bluetooth::Uuid::From16Bit(0x2BC6);
+} // namespace uuid
+
+namespace codec_spec_conf {
+/* LTV Types */
+constexpr uint8_t kLeAudioCodecLC3TypeSamplingFreq = 0x01;
+constexpr uint8_t kLeAudioCodecLC3TypeFrameDuration = 0x02;
+constexpr uint8_t kLeAudioCodecLC3TypeAudioChannelAllocation = 0x03;
+constexpr uint8_t kLeAudioCodecLC3TypeOctetPerFrame = 0x04;
+constexpr uint8_t kLeAudioCodecLC3TypeCodecFrameBlocksPerSdu = 0x05;
+
+/* Sampling Frequencies */
+constexpr uint8_t kLeAudioSamplingFreq8000Hz = 0x01;
+constexpr uint8_t kLeAudioSamplingFreq11025Hz = 0x02;
+constexpr uint8_t kLeAudioSamplingFreq16000Hz = 0x03;
+constexpr uint8_t kLeAudioSamplingFreq22050Hz = 0x04;
+constexpr uint8_t kLeAudioSamplingFreq24000Hz = 0x05;
+constexpr uint8_t kLeAudioSamplingFreq32000Hz = 0x06;
+constexpr uint8_t kLeAudioSamplingFreq44100Hz = 0x07;
+constexpr uint8_t kLeAudioSamplingFreq48000Hz = 0x08;
+constexpr uint8_t kLeAudioSamplingFreq88200Hz = 0x09;
+constexpr uint8_t kLeAudioSamplingFreq96000Hz = 0x0A;
+constexpr uint8_t kLeAudioSamplingFreq176400Hz = 0x0B;
+constexpr uint8_t kLeAudioSamplingFreq192000Hz = 0x0C;
+constexpr uint8_t kLeAudioSamplingFreq384000Hz = 0x0D;
+
+/* Frame Durations */
+constexpr uint8_t kLeAudioCodecLC3FrameDur7500us = 0x00;
+constexpr uint8_t kLeAudioCodecLC3FrameDur10000us = 0x01;
+
+/* Audio Allocations */
+constexpr uint32_t kLeAudioLocationMonoUnspecified = 0x00000000;
+constexpr uint32_t kLeAudioLocationFrontLeft = 0x00000001;
+constexpr uint32_t kLeAudioLocationFrontRight = 0x00000002;
+constexpr uint32_t kLeAudioLocationFrontCenter = 0x00000004;
+constexpr uint32_t kLeAudioLocationLowFreqEffects1 = 0x00000008;
+constexpr uint32_t kLeAudioLocationBackLeft = 0x00000010;
+constexpr uint32_t kLeAudioLocationBackRight = 0x00000020;
+constexpr uint32_t kLeAudioLocationFrontLeftOfCenter = 0x00000040;
+constexpr uint32_t kLeAudioLocationFrontRightOfCenter = 0x00000080;
+constexpr uint32_t kLeAudioLocationBackCenter = 0x00000100;
+constexpr uint32_t kLeAudioLocationLowFreqEffects2 = 0x00000200;
+constexpr uint32_t kLeAudioLocationSideLeft = 0x00000400;
+constexpr uint32_t kLeAudioLocationSideRight = 0x00000800;
+constexpr uint32_t kLeAudioLocationTopFrontLeft = 0x00001000;
+constexpr uint32_t kLeAudioLocationTopFrontRight = 0x00002000;
+constexpr uint32_t kLeAudioLocationTopFrontCenter = 0x00004000;
+constexpr uint32_t kLeAudioLocationTopCenter = 0x00008000;
+constexpr uint32_t kLeAudioLocationTopBackLeft = 0x00010000;
+constexpr uint32_t kLeAudioLocationTopBackRight = 0x00020000;
+constexpr uint32_t kLeAudioLocationTopSideLeft = 0x00040000;
+constexpr uint32_t kLeAudioLocationTopSideRight = 0x00080000;
+constexpr uint32_t kLeAudioLocationTopSideCenter = 0x00100000;
+constexpr uint32_t kLeAudioLocationBottomFrontCenter = 0x00200000;
+constexpr uint32_t kLeAudioLocationBottomFrontLeft = 0x00400000;
+constexpr uint32_t kLeAudioLocationBottomFrontRight = 0x00800000;
+constexpr uint32_t kLeAudioLocationFrontLeftWide = 0x01000000;
+constexpr uint32_t kLeAudioLocationFrontRightWide = 0x02000000;
+constexpr uint32_t kLeAudioLocationLeftSurround = 0x04000000;
+constexpr uint32_t kLeAudioLocationRightSurround = 0x08000000;
+
+constexpr uint32_t kLeAudioLocationAnyLeft =
+ kLeAudioLocationFrontLeft | kLeAudioLocationBackLeft |
+ kLeAudioLocationFrontLeftOfCenter | kLeAudioLocationSideLeft |
+ kLeAudioLocationTopFrontLeft | kLeAudioLocationTopBackLeft |
+ kLeAudioLocationTopSideLeft | kLeAudioLocationBottomFrontLeft |
+ kLeAudioLocationFrontLeftWide | kLeAudioLocationLeftSurround;
+
+constexpr uint32_t kLeAudioLocationAnyRight =
+ kLeAudioLocationFrontRight | kLeAudioLocationBackRight |
+ kLeAudioLocationFrontRightOfCenter | kLeAudioLocationSideRight |
+ kLeAudioLocationTopFrontRight | kLeAudioLocationTopBackRight |
+ kLeAudioLocationTopSideRight | kLeAudioLocationBottomFrontRight |
+ kLeAudioLocationFrontRightWide | kLeAudioLocationRightSurround;
+
+constexpr uint32_t kLeAudioLocationStereo =
+ kLeAudioLocationFrontLeft | kLeAudioLocationFrontRight;
+
+/* Octets Per Frame */
+constexpr uint16_t kLeAudioCodecLC3FrameLen30 = 30;
+constexpr uint16_t kLeAudioCodecLC3FrameLen40 = 40;
+constexpr uint16_t kLeAudioCodecLC3FrameLen120 = 120;
+
+} // namespace codec_spec_conf
+
+constexpr uint8_t kInvalidCisId = 0xFF;
+
+namespace codec_spec_caps {
+uint16_t constexpr SamplingFreqConfig2Capability(uint8_t conf) {
+ return (1 << (conf - 1));
+}
+
+uint8_t constexpr FrameDurationConfig2Capability(uint8_t conf) {
+ return (0x01 << (conf));
+}
+
+inline uint8_t GetAudioChannelCounts(std::bitset<32> allocation) {
+ /*
+ * BAP d09r07 B4.2.3 Audio_Channel_Allocation
+ * "(...) Audio_Channel_Allocation bitmap value of all zeros or the
+ * absence of the Audio_Channel_Allocation LTV structure within a
+ * Codec_Specific_Configuration field shall be interpreted as defining a
+ * single audio channel of Mono audio (a single channel of no specified
+ * Audio Location).
+ */
+ uint8_t audio_channel_counts = allocation.count() ?: 1;
+ return (0x01 << (audio_channel_counts - 1));
+}
+
+/* LTV Types - same values as in Codec Specific Configurations but 0x03 is
+ * named differently.
+ */
+constexpr uint8_t kLeAudioCodecLC3TypeSamplingFreq =
+ codec_spec_conf::kLeAudioCodecLC3TypeSamplingFreq;
+constexpr uint8_t kLeAudioCodecLC3TypeFrameDuration =
+ codec_spec_conf::kLeAudioCodecLC3TypeFrameDuration;
+constexpr uint8_t kLeAudioCodecLC3TypeAudioChannelCounts =
+ codec_spec_conf::kLeAudioCodecLC3TypeAudioChannelAllocation;
+constexpr uint8_t kLeAudioCodecLC3TypeOctetPerFrame =
+ codec_spec_conf::kLeAudioCodecLC3TypeOctetPerFrame;
+constexpr uint8_t kLeAudioCodecLC3TypeMaxCodecFramesPerSdu =
+ codec_spec_conf::kLeAudioCodecLC3TypeCodecFrameBlocksPerSdu;
+
+/* Sampling Frequencies */
+constexpr uint16_t kLeAudioSamplingFreq8000Hz =
+ SamplingFreqConfig2Capability(codec_spec_conf::kLeAudioSamplingFreq8000Hz);
+constexpr uint16_t kLeAudioSamplingFreq16000Hz =
+ SamplingFreqConfig2Capability(codec_spec_conf::kLeAudioSamplingFreq16000Hz);
+constexpr uint16_t kLeAudioSamplingFreq24000Hz =
+ SamplingFreqConfig2Capability(codec_spec_conf::kLeAudioSamplingFreq24000Hz);
+constexpr uint16_t kLeAudioSamplingFreq32000Hz =
+ SamplingFreqConfig2Capability(codec_spec_conf::kLeAudioSamplingFreq32000Hz);
+constexpr uint16_t kLeAudioSamplingFreq44100Hz =
+ SamplingFreqConfig2Capability(codec_spec_conf::kLeAudioSamplingFreq44100Hz);
+constexpr uint16_t kLeAudioSamplingFreq48000Hz =
+ SamplingFreqConfig2Capability(codec_spec_conf::kLeAudioSamplingFreq48000Hz);
+
+/* Frame Durations */
+constexpr uint8_t kLeAudioCodecLC3FrameDur7500us =
+ FrameDurationConfig2Capability(
+ codec_spec_conf::kLeAudioCodecLC3FrameDur7500us);
+constexpr uint8_t kLeAudioCodecLC3FrameDur10000us =
+ FrameDurationConfig2Capability(
+ codec_spec_conf::kLeAudioCodecLC3FrameDur10000us);
+constexpr uint8_t kLeAudioCodecLC3FrameDurPrefer7500us = 0x10;
+constexpr uint8_t kLeAudioCodecLC3FrameDurPrefer10000us = 0x20;
+
+/* Audio Channel Counts */
+/* Each bit represents support for additional channel: bit 0 - one channel,
+ * bit 1 - two, bit 3 - four channels. Multiple bits can be enabled at once.
+ */
+constexpr uint8_t kLeAudioCodecLC3ChannelCountNone = 0x00;
+constexpr uint8_t kLeAudioCodecLC3ChannelCountSingleChannel = 0x01;
+constexpr uint8_t kLeAudioCodecLC3ChannelCountTwoChannel = 0x02;
+
+/* Octets Per Frame - same as in Codec Specific Configurations but in
+ * capabilities we get two values: min and max.
+ */
+constexpr uint16_t kLeAudioCodecLC3FrameLen30 =
+ codec_spec_conf::kLeAudioCodecLC3FrameLen30;
+constexpr uint16_t kLeAudioCodecLC3FrameLen40 =
+ codec_spec_conf::kLeAudioCodecLC3FrameLen40;
+constexpr uint16_t kLeAudioCodecLC3FrameLen120 =
+ codec_spec_conf::kLeAudioCodecLC3FrameLen120;
+
+}; // namespace codec_spec_caps
+
+namespace types {
+constexpr uint8_t kLeAudioCodingFormatLC3 = bluetooth::hci::kIsoCodingFormatLc3;
+constexpr uint8_t kLeAudioCodingFormatVendorSpecific =
+ bluetooth::hci::kIsoCodingFormatVendorSpecific;
+constexpr uint16_t kLeAudioVendorCompanyIdUndefined = 0x00;
+constexpr uint16_t kLeAudioVendorCodecIdUndefined = 0x00;
+
+/* Metadata types from Assigned Numbers */
+constexpr uint8_t kLeAudioMetadataTypePreferredAudioContext = 0x01;
+constexpr uint8_t kLeAudioMetadataTypeStreamingAudioContext = 0x02;
+
+/* CSIS Types */
+constexpr uint8_t kDefaultScanDurationS = 5;
+constexpr uint8_t kDefaultCsisSetSize = 2;
+
+constexpr uint8_t kLeAudioDirectionSink = 0x01;
+constexpr uint8_t kLeAudioDirectionSource = 0x02;
+
+/* Audio stream config types */
+constexpr uint8_t kFramingUnframedPduSupported = 0x00;
+constexpr uint8_t kFramingUnframedPduUnsupported = 0x01;
+
+constexpr uint8_t kTargetLatencyLower = 0x01;
+constexpr uint8_t kTargetLatencyBalancedLatencyReliability = 0x02;
+constexpr uint8_t kTargetLatencyHigherReliability = 0x03;
+
+constexpr uint8_t kTargetPhy1M = 0x01;
+constexpr uint8_t kTargetPhy2M = 0x02;
+constexpr uint8_t kTargetPhyCoded = 0x03;
+
+constexpr uint32_t kPresDelayNoPreference = 0x00000000;
+
+constexpr uint16_t kMaxTransportLatencyMin = 0x0005;
+constexpr uint16_t kMaxTransportLatencyMax = 0x0FA0;
+
+/* Enums */
+enum class CsisLockState : uint8_t {
+ CSIS_STATE_UNSET = 0x00,
+ CSIS_STATE_UNLOCKED,
+ CSIS_STATE_LOCKED
+};
+
+enum class CsisDiscoveryState : uint8_t {
+ CSIS_DISCOVERY_IDLE,
+ CSIS_DISCOVERY_ONGOING,
+ CSIS_DISCOVERY_COMPLETED,
+};
+
+/* ASE states according to BAP defined state machine states */
+enum class AseState : uint8_t {
+ BTA_LE_AUDIO_ASE_STATE_IDLE = 0x00,
+ BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED = 0x01,
+ BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED = 0x02,
+ BTA_LE_AUDIO_ASE_STATE_ENABLING = 0x03,
+ BTA_LE_AUDIO_ASE_STATE_STREAMING = 0x04,
+ BTA_LE_AUDIO_ASE_STATE_DISABLING = 0x05,
+ BTA_LE_AUDIO_ASE_STATE_RELEASING = 0x06,
+};
+
+enum class AudioStreamDataPathState {
+ IDLE,
+ CIS_DISCONNECTING,
+ CIS_ASSIGNED,
+ CIS_PENDING,
+ CIS_ESTABLISHED,
+ DATA_PATH_ESTABLISHED,
+};
+
+/* Context Types */
+enum class LeAudioContextType : uint16_t {
+ UNSPECIFIED = 0x0001,
+ CONVERSATIONAL = 0x0002,
+ MEDIA = 0x0004,
+ GAME = 0x0008,
+ INSTRUCTIONAL = 0x0010,
+ VOICEASSISTANTS = 0x0020,
+ LIVE = 0x0040,
+ SOUNDEFFECTS = 0x0080,
+ NOTIFICATIONS = 0x0100,
+ RINGTONE = 0x0200,
+ ALERTS = 0x0400,
+ EMERGENCYALARM = 0x0800,
+ RFU = 0x1000,
+};
+
+/* Configuration strategy */
+enum class LeAudioConfigurationStrategy : uint8_t {
+ MONO_ONE_CIS_PER_DEVICE = 0x00, /* Common true wireless speakers */
+ STEREO_TWO_CISES_PER_DEVICE =
+ 0x01, /* Requires 2 ASEs and 2 Audio Allocation for left/right */
+ STEREO_ONE_CIS_PER_DEVICE = 0x02, /* Requires channel count 2*/
+ RFU = 0x03,
+};
+
+constexpr LeAudioContextType operator|(LeAudioContextType lhs,
+ LeAudioContextType rhs) {
+ return static_cast<LeAudioContextType>(
+ static_cast<std::underlying_type<LeAudioContextType>::type>(lhs) |
+ static_cast<std::underlying_type<LeAudioContextType>::type>(rhs));
+}
+
+constexpr LeAudioContextType kLeAudioContextAllTypesArray[] = {
+ LeAudioContextType::UNSPECIFIED, LeAudioContextType::CONVERSATIONAL,
+ LeAudioContextType::MEDIA, LeAudioContextType::GAME,
+ LeAudioContextType::INSTRUCTIONAL, LeAudioContextType::VOICEASSISTANTS,
+ LeAudioContextType::LIVE, LeAudioContextType::SOUNDEFFECTS,
+ LeAudioContextType::NOTIFICATIONS, LeAudioContextType::RINGTONE,
+ LeAudioContextType::ALERTS, LeAudioContextType::EMERGENCYALARM,
+};
+
+constexpr LeAudioContextType kLeAudioContextAllTypes =
+ LeAudioContextType::UNSPECIFIED | LeAudioContextType::CONVERSATIONAL |
+ LeAudioContextType::MEDIA | LeAudioContextType::GAME |
+ LeAudioContextType::INSTRUCTIONAL | LeAudioContextType::VOICEASSISTANTS |
+ LeAudioContextType::LIVE | LeAudioContextType::SOUNDEFFECTS |
+ LeAudioContextType::NOTIFICATIONS | LeAudioContextType::RINGTONE |
+ LeAudioContextType::ALERTS | LeAudioContextType::EMERGENCYALARM;
+
+/* Structures */
+class LeAudioLtvMap {
+ public:
+ LeAudioLtvMap() {}
+ LeAudioLtvMap(std::map<uint8_t, std::vector<uint8_t>> values)
+ : values(std::move(values)) {}
+
+ std::optional<std::vector<uint8_t>> Find(uint8_t type) const;
+ bool IsEmpty() const { return values.empty(); }
+ void Clear() { values.clear(); }
+ size_t Size() const { return values.size(); }
+ const std::map<uint8_t, std::vector<uint8_t>>& Values() const {
+ return values;
+ }
+ std::string ToString() const;
+ size_t RawPacketSize() const;
+ uint8_t* RawPacket(uint8_t* p_buf) const;
+ static LeAudioLtvMap Parse(const uint8_t* value, uint8_t len, bool& success);
+
+ private:
+ std::map<uint8_t, std::vector<uint8_t>> values;
+};
+
+struct LeAudioLc3Config {
+ static const std::map<uint8_t, uint32_t> sampling_freq_map;
+ static const std::map<uint8_t, uint32_t> frame_duration_map;
+
+ uint8_t sampling_frequency;
+ uint8_t frame_duration;
+ uint16_t octets_per_codec_frame;
+ uint32_t audio_channel_allocation;
+ uint8_t channel_count;
+
+ /** Returns the sampling frequency representation in Hz */
+ uint32_t GetSamplingFrequencyHz() const {
+ return sampling_freq_map.count(sampling_frequency)
+ ? sampling_freq_map.at(sampling_frequency)
+ : 0;
+ }
+
+ /** Returns the frame duration representation in us */
+ uint32_t GetFrameDurationUs() const {
+ return frame_duration_map.count(frame_duration)
+ ? frame_duration_map.at(frame_duration)
+ : 0;
+ }
+
+ uint8_t GetChannelCount(void) const { return channel_count; }
+
+ LeAudioLtvMap GetAsLtvMap() const {
+ return LeAudioLtvMap({
+ {codec_spec_conf::kLeAudioCodecLC3TypeSamplingFreq,
+ UINT8_TO_VEC_UINT8(sampling_frequency)},
+ {codec_spec_conf::kLeAudioCodecLC3TypeFrameDuration,
+ UINT8_TO_VEC_UINT8(frame_duration)},
+ {codec_spec_conf::kLeAudioCodecLC3TypeAudioChannelAllocation,
+ UINT32_TO_VEC_UINT8(audio_channel_allocation)},
+ {codec_spec_conf::kLeAudioCodecLC3TypeOctetPerFrame,
+ UINT16_TO_VEC_UINT8(octets_per_codec_frame)},
+ });
+ }
+};
+
+struct LeAudioCodecId {
+ uint8_t coding_format;
+ uint16_t vendor_company_id;
+ uint16_t vendor_codec_id;
+
+ friend bool operator==(const LeAudioCodecId& lhs, const LeAudioCodecId& rhs) {
+ if (lhs.coding_format != rhs.coding_format) return false;
+
+ if (lhs.coding_format == kLeAudioCodingFormatVendorSpecific &&
+ (lhs.vendor_company_id != rhs.vendor_company_id ||
+ lhs.vendor_codec_id != rhs.vendor_codec_id))
+ return false;
+
+ return true;
+ }
+
+ friend bool operator!=(const LeAudioCodecId& lhs, const LeAudioCodecId& rhs) {
+ return !(lhs == rhs);
+ }
+};
+
+struct hdl_pair {
+ hdl_pair() = default;
+ hdl_pair(uint16_t val_hdl, uint16_t ccc_hdl)
+ : val_hdl(val_hdl), ccc_hdl(ccc_hdl) {}
+
+ uint16_t val_hdl = 0;
+ uint16_t ccc_hdl = 0;
+};
+
+struct ase {
+ static constexpr uint8_t kAseIdInvalid = 0x00;
+
+ ase(uint16_t val_hdl, uint16_t ccc_hdl, uint8_t direction)
+ : hdls(val_hdl, ccc_hdl),
+ id(kAseIdInvalid),
+ cis_id(kInvalidCisId),
+ direction(direction),
+ active(false),
+ reconfigure(false),
+ data_path_state(AudioStreamDataPathState::IDLE),
+ preferred_phy(0),
+ state(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {}
+
+ struct hdl_pair hdls;
+ uint8_t id;
+ uint8_t cis_id;
+ const uint8_t direction;
+ uint16_t cis_conn_hdl = 0;
+
+ bool active;
+ bool reconfigure;
+ AudioStreamDataPathState data_path_state;
+
+ /* Codec configuration */
+ LeAudioCodecId codec_id;
+ LeAudioLc3Config codec_config;
+ uint8_t framing;
+ uint8_t preferred_phy;
+
+ /* Qos configuration */
+ uint16_t max_sdu_size;
+ uint8_t retrans_nb;
+ uint16_t max_transport_latency;
+ uint32_t pres_delay_min;
+ uint32_t pres_delay_max;
+ uint32_t preferred_pres_delay_min;
+ uint32_t preferred_pres_delay_max;
+
+ std::vector<uint8_t> metadata;
+
+ AseState state;
+};
+
+struct BidirectAsesPair {
+ struct ase* sink;
+ struct ase* source;
+};
+
+struct acs_ac_record {
+ LeAudioCodecId codec_id;
+ LeAudioLtvMap codec_spec_caps;
+ std::vector<uint8_t> metadata;
+};
+
+using PublishedAudioCapabilities =
+ std::vector<std::tuple<hdl_pair, std::vector<acs_ac_record>>>;
+using AudioLocations = std::bitset<32>;
+using AudioContexts = std::bitset<16>;
+
+} // namespace types
+
+namespace set_configurations {
+
+struct CodecCapabilitySetting {
+ types::LeAudioCodecId id;
+
+ /* Codec Specific Configuration variant */
+ std::variant<types::LeAudioLc3Config> config;
+
+ /* Sampling freqency requested for codec */
+ uint32_t GetConfigSamplingFrequency() const;
+ /* Data fetch/feed interval for codec in microseconds */
+ uint32_t GetConfigDataIntervalUs() const;
+ /* Audio bit depth required for codec */
+ uint8_t GetConfigBitsPerSample() const;
+ /* Audio channels number for stream */
+ uint8_t GetConfigChannelCount() const;
+};
+
+struct SetConfiguration {
+ SetConfiguration(uint8_t direction, uint8_t device_cnt, uint8_t ase_cnt,
+ CodecCapabilitySetting codec,
+ le_audio::types::LeAudioConfigurationStrategy strategy =
+ le_audio::types::LeAudioConfigurationStrategy::
+ MONO_ONE_CIS_PER_DEVICE)
+ : direction(direction),
+ device_cnt(device_cnt),
+ ase_cnt(ase_cnt),
+ codec(codec),
+ strategy(strategy) {}
+
+ uint8_t direction; /* Direction of set */
+ uint8_t device_cnt; /* How many devices must be in set */
+ uint8_t ase_cnt; /* How many ASE we need in configuration */
+ CodecCapabilitySetting codec;
+ types::LeAudioConfigurationStrategy strategy;
+};
+
+/* Defined audio scenarios */
+struct AudioSetConfiguration {
+ std::string name;
+ std::vector<struct SetConfiguration> confs;
+};
+
+using AudioSetConfigurations = std::vector<const AudioSetConfiguration*>;
+
+const types::LeAudioCodecId LeAudioCodecIdLc3 = {
+ .coding_format = types::kLeAudioCodingFormatLC3,
+ .vendor_company_id = types::kLeAudioVendorCompanyIdUndefined,
+ .vendor_codec_id = types::kLeAudioVendorCodecIdUndefined};
+
+static constexpr uint32_t kChannelAllocationMono =
+ codec_spec_conf::kLeAudioLocationMonoUnspecified;
+static constexpr uint32_t kChannelAllocationStereo =
+ codec_spec_conf::kLeAudioLocationFrontLeft |
+ codec_spec_conf::kLeAudioLocationFrontRight;
+
+/**
+ * Supported audio codec capability settings
+ *
+ * The subset of capabilities defined in BAP_Validation_r13 Table 3.6.
+ */
+constexpr CodecCapabilitySetting codec_lc3_16_1(uint8_t channel_count) {
+ return CodecCapabilitySetting{
+ .id = LeAudioCodecIdLc3,
+ .config = types::LeAudioLc3Config({
+ .sampling_frequency = codec_spec_conf::kLeAudioSamplingFreq16000Hz,
+ .frame_duration = codec_spec_conf::kLeAudioCodecLC3FrameDur7500us,
+ .octets_per_codec_frame = codec_spec_conf::kLeAudioCodecLC3FrameLen30,
+ .channel_count = channel_count,
+ .audio_channel_allocation = 0,
+ })};
+}
+
+constexpr CodecCapabilitySetting codec_lc3_16_2(uint8_t channel_count) {
+ return CodecCapabilitySetting{
+ .id = LeAudioCodecIdLc3,
+ .config = types::LeAudioLc3Config({
+ .sampling_frequency = codec_spec_conf::kLeAudioSamplingFreq16000Hz,
+ .frame_duration = codec_spec_conf::kLeAudioCodecLC3FrameDur10000us,
+ .octets_per_codec_frame = codec_spec_conf::kLeAudioCodecLC3FrameLen40,
+ .channel_count = channel_count,
+ .audio_channel_allocation = 0,
+ })};
+}
+
+constexpr CodecCapabilitySetting codec_lc3_48_4(uint8_t channel_count) {
+ return CodecCapabilitySetting{
+ .id = LeAudioCodecIdLc3,
+ .config = types::LeAudioLc3Config({
+ .sampling_frequency = codec_spec_conf::kLeAudioSamplingFreq48000Hz,
+ .frame_duration = codec_spec_conf::kLeAudioCodecLC3FrameDur10000us,
+ .octets_per_codec_frame =
+ codec_spec_conf::kLeAudioCodecLC3FrameLen120,
+ .channel_count = channel_count,
+ .audio_channel_allocation = 0,
+ })};
+}
+
+/*
+ * AudioSetConfiguration defines the audio set configuration and codec settings
+ * to to be used by le audio policy to match the required configuration with
+ * audio server capabilities. The codec settings are defined with respect to
+ * "Broadcast Source audio capability configuration support requirements"
+ * defined in BAP d09r06
+ */
+const AudioSetConfiguration kSingleDev_OneChanMonoSnk_16_2 = {
+ .name = "kSingleDev_OneChanMonoSnk_16_2",
+ .confs = {SetConfiguration(
+ types::kLeAudioDirectionSink, 1, 1,
+ codec_lc3_16_2(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel))}};
+
+const AudioSetConfiguration kSingleDev_OneChanMonoSnk_16_1 = {
+ .name = "kSingleDev_OneChanMonoSnk_16_1",
+ .confs = {SetConfiguration(
+ types::kLeAudioDirectionSink, 1, 1,
+ codec_lc3_16_1(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel))}};
+
+const AudioSetConfiguration kSingleDev_TwoChanStereoSnk_16_1 = {
+ .name = "kSingleDev_TwoChanStereoSnk_16_1",
+ .confs = {SetConfiguration(
+ types::kLeAudioDirectionSink, 1, 1,
+ codec_lc3_16_1(codec_spec_caps::kLeAudioCodecLC3ChannelCountTwoChannel),
+ le_audio::types::LeAudioConfigurationStrategy::
+ STEREO_ONE_CIS_PER_DEVICE)}};
+
+const AudioSetConfiguration kSingleDev_OneChanStereoSnk_16_1 = {
+ .name = "kSingleDev_OneChanStereoSnk_16_1",
+ .confs = {SetConfiguration(
+ types::kLeAudioDirectionSink, 1, 2,
+ codec_lc3_16_1(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel),
+ le_audio::types::LeAudioConfigurationStrategy::
+ STEREO_TWO_CISES_PER_DEVICE)}};
+
+const AudioSetConfiguration kDualDev_OneChanStereoSnk_16_1 = {
+ .name = "kDualDev_OneChanStereoSnk_16_1",
+ .confs = {SetConfiguration(
+ types::kLeAudioDirectionSink, 2, 2,
+ codec_lc3_16_1(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel))}};
+
+const AudioSetConfiguration kSingleDev_TwoChanStereoSnk_48_4 = {
+ .name = "kSingleDev_TwoChanStereoSnk_48_4",
+ .confs = {SetConfiguration(
+ types::kLeAudioDirectionSink, 1, 1,
+ codec_lc3_48_4(codec_spec_caps::kLeAudioCodecLC3ChannelCountTwoChannel),
+ le_audio::types::LeAudioConfigurationStrategy::
+ STEREO_ONE_CIS_PER_DEVICE)}};
+
+const AudioSetConfiguration kDualDev_OneChanStereoSnk_48_4 = {
+ .name = "kDualDev_OneChanStereoSnk_48_4",
+ .confs = {SetConfiguration(
+ types::kLeAudioDirectionSink, 2, 2,
+ codec_lc3_48_4(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel))}};
+
+const AudioSetConfiguration kSingleDev_OneChanStereoSnk_48_4 = {
+ .name = "kSingleDev_OneChanStereoSnk_48_4",
+ .confs = {SetConfiguration(
+ types::kLeAudioDirectionSink, 1, 2,
+ codec_lc3_48_4(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel),
+ le_audio::types::LeAudioConfigurationStrategy::
+ STEREO_TWO_CISES_PER_DEVICE)}};
+
+const AudioSetConfiguration kSingleDev_OneChanMonoSnk_48_4 = {
+ .name = "kSingleDev_OneChanMonoSnk_48_4",
+ .confs = {SetConfiguration(
+ types::kLeAudioDirectionSink, 1, 1,
+ codec_lc3_48_4(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel))}};
+
+const AudioSetConfiguration kSingleDev_TwoChanStereoSnk_16_2 = {
+ .name = "kSingleDev_TwoChanStereoSnk_16_2",
+ .confs = {SetConfiguration(
+ types::kLeAudioDirectionSink, 1, 1,
+ codec_lc3_16_2(codec_spec_caps::kLeAudioCodecLC3ChannelCountTwoChannel),
+ le_audio::types::LeAudioConfigurationStrategy::
+ STEREO_ONE_CIS_PER_DEVICE)}};
+
+const AudioSetConfiguration kSingleDev_OneChanStereoSnk_16_2 = {
+ .name = "kSingleDev_OneChanStereoSnk_16_2",
+ .confs = {SetConfiguration(
+ types::kLeAudioDirectionSink, 1, 2,
+ codec_lc3_16_2(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel),
+ le_audio::types::LeAudioConfigurationStrategy::
+ STEREO_TWO_CISES_PER_DEVICE)}};
+
+const AudioSetConfiguration kDualDev_OneChanStereoSnk_16_2 = {
+ .name = "kDualDev_OneChanStereoSnk_16_2",
+ .confs = {SetConfiguration(
+ types::kLeAudioDirectionSink, 2, 2,
+ codec_lc3_16_2(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel))}};
+
+const AudioSetConfiguration kSingleDev_OneChanMonoSnk_OneChanMonoSrc_16_1 = {
+ .name = "kSingleDev_OneChanMonoSnk_OneChanMonoSrc_16_1",
+ .confs = {
+ SetConfiguration(
+ types::kLeAudioDirectionSink, 1, 1,
+ codec_lc3_16_1(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel)),
+ SetConfiguration(
+ types::kLeAudioDirectionSource, 1, 1,
+ codec_lc3_16_1(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel))}};
+
+const AudioSetConfiguration kSingleDev_OneChanMonoSnk_OneChanMonoSrc_16_2 = {
+ .name = "kSingleDev_OneChanMonoSnk_OneChanMonoSrc_16_2",
+ .confs = {
+ SetConfiguration(
+ types::kLeAudioDirectionSink, 1, 1,
+ codec_lc3_16_2(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel)),
+ SetConfiguration(
+ types::kLeAudioDirectionSource, 1, 1,
+ codec_lc3_16_2(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel))}};
+
+const AudioSetConfiguration kSingleDev_TwoChanStereoSnk_OneChanMonoSrc_16_2 = {
+ .name = "kSingleDev_TwoChanStereoSnk_OneChanMonoSrc_16_2",
+ .confs = {
+ SetConfiguration(
+ types::kLeAudioDirectionSink, 1, 1,
+ codec_lc3_16_2(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountTwoChannel),
+ le_audio::types::LeAudioConfigurationStrategy::
+ STEREO_ONE_CIS_PER_DEVICE),
+ SetConfiguration(
+ types::kLeAudioDirectionSource, 1, 1,
+ codec_lc3_16_2(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel))}};
+
+const AudioSetConfiguration
+ kDualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_16_2 = {
+ .name = "kDualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_16_2",
+ .confs = {
+ SetConfiguration(
+ types::kLeAudioDirectionSink, 2, 4,
+ codec_lc3_16_2(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel),
+ le_audio::types::LeAudioConfigurationStrategy::
+ STEREO_TWO_CISES_PER_DEVICE),
+ SetConfiguration(
+ types::kLeAudioDirectionSource, 1, 1,
+ codec_lc3_16_2(
+ codec_spec_caps::
+ kLeAudioCodecLC3ChannelCountSingleChannel))}};
+
+const AudioSetConfiguration kSingleDev_OneChanStereoSnk_OneChanMonoSrc_16_2 = {
+ .name = "kSingleDev_OneChanStereoSnk_OneChanMonoSrc_16_2",
+ .confs = {
+ SetConfiguration(
+ types::kLeAudioDirectionSink, 1, 2,
+ codec_lc3_16_2(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel),
+ le_audio::types::LeAudioConfigurationStrategy::
+ STEREO_TWO_CISES_PER_DEVICE),
+ SetConfiguration(
+ types::kLeAudioDirectionSource, 1, 1,
+ codec_lc3_16_2(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel))}};
+
+const AudioSetConfiguration kDualDev_OneChanStereoSnk_OneChanMonoSrc_16_2 = {
+ .name = "kDualDev_OneChanStereoSnk_OneChanMonoSrc_16_2",
+ .confs = {
+ SetConfiguration(
+ types::kLeAudioDirectionSink, 2, 2,
+ codec_lc3_16_2(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel)),
+ SetConfiguration(
+ types::kLeAudioDirectionSource, 1, 1,
+ codec_lc3_16_2(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel))}};
+
+const AudioSetConfiguration kSingleDev_TwoChanStereoSnk_OneChanMonoSrc_16_1 = {
+ .name = "kSingleDev_TwoChanStereoSnk_OneChanMonoSrc_16_1",
+ .confs = {
+ SetConfiguration(
+ types::kLeAudioDirectionSink, 1, 1,
+ codec_lc3_16_1(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountTwoChannel),
+ le_audio::types::LeAudioConfigurationStrategy::
+ STEREO_ONE_CIS_PER_DEVICE),
+ SetConfiguration(
+ types::kLeAudioDirectionSource, 1, 1,
+ codec_lc3_16_1(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel))}};
+
+const AudioSetConfiguration kSingleDev_OneChanStereoSnk_OneChanMonoSrc_16_1 = {
+ .name = "kSingleDev_OneChanStereoSnk_OneChanMonoSrc_16_1",
+ .confs = {
+ SetConfiguration(
+ types::kLeAudioDirectionSink, 1, 2,
+ codec_lc3_16_1(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel),
+ le_audio::types::LeAudioConfigurationStrategy::
+ STEREO_TWO_CISES_PER_DEVICE),
+ SetConfiguration(
+ types::kLeAudioDirectionSource, 1, 1,
+ codec_lc3_16_1(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel))}};
+
+const AudioSetConfiguration kDualDev_OneChanStereoSnk_OneChanMonoSrc_16_1 = {
+ .name = "kDualDev_OneChanStereoSnk_OneChanMonoSrc_16_1",
+ .confs = {
+ SetConfiguration(
+ types::kLeAudioDirectionSink, 2, 2,
+ codec_lc3_16_1(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel)),
+ SetConfiguration(
+ types::kLeAudioDirectionSource, 1, 1,
+ codec_lc3_16_1(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel))}};
+
+const AudioSetConfiguration
+ kDualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_16_1 = {
+ .name = "kDualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_16_1",
+ .confs = {
+ SetConfiguration(
+ types::kLeAudioDirectionSink, 2, 4,
+ codec_lc3_16_1(
+ codec_spec_caps::kLeAudioCodecLC3ChannelCountSingleChannel),
+ le_audio::types::LeAudioConfigurationStrategy::
+ STEREO_TWO_CISES_PER_DEVICE),
+ SetConfiguration(
+ types::kLeAudioDirectionSource, 1, 1,
+ codec_lc3_16_1(
+ codec_spec_caps::
+ kLeAudioCodecLC3ChannelCountSingleChannel))}};
+
+/* Defined audio scenario linked with context type, priority sorted */
+const AudioSetConfigurations audio_set_conf_ringtone = {
+ &kDualDev_OneChanStereoSnk_16_2, &kDualDev_OneChanStereoSnk_16_1,
+ &kSingleDev_OneChanStereoSnk_16_2, &kSingleDev_OneChanStereoSnk_16_1,
+ &kSingleDev_TwoChanStereoSnk_16_2, &kSingleDev_TwoChanStereoSnk_16_1,
+ &kSingleDev_OneChanMonoSnk_16_2, &kSingleDev_OneChanMonoSnk_16_1,
+};
+
+const AudioSetConfigurations audio_set_conf_conversational = {
+ &kDualDev_OneChanStereoSnk_OneChanMonoSrc_16_2,
+ &kDualDev_OneChanStereoSnk_OneChanMonoSrc_16_1,
+ &kDualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_16_2,
+ &kDualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_16_1,
+ &kSingleDev_TwoChanStereoSnk_OneChanMonoSrc_16_2,
+ &kSingleDev_TwoChanStereoSnk_OneChanMonoSrc_16_1,
+ &kSingleDev_OneChanStereoSnk_OneChanMonoSrc_16_2,
+ &kSingleDev_OneChanStereoSnk_OneChanMonoSrc_16_1,
+ &kSingleDev_OneChanMonoSnk_OneChanMonoSrc_16_2,
+ &kSingleDev_OneChanMonoSnk_OneChanMonoSrc_16_1,
+};
+
+const AudioSetConfigurations audio_set_conf_media = {
+ &kDualDev_OneChanStereoSnk_48_4, &kDualDev_OneChanStereoSnk_16_2,
+ &kDualDev_OneChanStereoSnk_16_1, &kSingleDev_OneChanStereoSnk_48_4,
+ &kSingleDev_OneChanStereoSnk_16_2, &kSingleDev_OneChanStereoSnk_16_1,
+ &kSingleDev_TwoChanStereoSnk_48_4, &kSingleDev_TwoChanStereoSnk_16_2,
+ &kSingleDev_TwoChanStereoSnk_16_1, &kSingleDev_OneChanMonoSnk_48_4,
+ &kSingleDev_OneChanMonoSnk_16_2, &kSingleDev_OneChanMonoSnk_16_1,
+};
+
+const AudioSetConfigurations audio_set_conf_default = {
+ &kDualDev_OneChanStereoSnk_16_2,
+ &kSingleDev_OneChanStereoSnk_16_2,
+ &kSingleDev_TwoChanStereoSnk_16_2,
+ &kSingleDev_OneChanMonoSnk_16_2,
+};
+
+/* Declarations */
+bool check_if_may_cover_scenario(
+ const AudioSetConfigurations* audio_set_configurations, uint8_t group_size);
+bool check_if_may_cover_scenario(
+ const AudioSetConfiguration* audio_set_configuration, uint8_t group_size);
+bool IsCodecCapabilitySettingSupported(
+ const types::acs_ac_record& pac_record,
+ const CodecCapabilitySetting& codec_capability_setting);
+const AudioSetConfigurations* get_confs_by_type(types::LeAudioContextType type);
+} // namespace set_configurations
+
+struct stream_configuration {
+ bool valid;
+
+ types::LeAudioCodecId id;
+
+ /* Pointer to chosen req */
+ const le_audio::set_configurations::AudioSetConfiguration* conf;
+
+ /* Sink configuration */
+ /* For now we have always same frequency for all the channels */
+ uint32_t sink_sample_frequency_hz;
+ uint32_t sink_frame_duration_us;
+ uint16_t sink_octets_per_codec_frame;
+ /* Number of channels is what we will request from audio framework */
+ uint8_t sink_num_of_channels;
+ int sink_num_of_devices;
+ /* cis_handle, audio location*/
+ std::vector<std::pair<uint16_t, uint32_t>> sink_streams;
+
+ /* Source configuration */
+ /* For now we have always same frequency for all the channels */
+ uint32_t source_sample_frequency_hz;
+ uint32_t source_frame_duration_us;
+ uint16_t source_octets_per_codec_frame;
+ /* Number of channels is what we will request from audio framework */
+ uint8_t source_num_of_channels;
+ int source_num_of_devices;
+ /* cis_handle, audio location*/
+ std::vector<std::pair<uint16_t, uint32_t>> source_streams;
+};
+
+} // namespace le_audio
+
+std::ostream& operator<<(std::ostream& os,
+ const le_audio::types::LeAudioLc3Config& config);
+
+std::ostream& operator<<(std::ostream& os,
+ const le_audio::types::AseState& state);
diff --git a/bta/le_audio/le_audio_types_test.cc b/bta/le_audio/le_audio_types_test.cc
new file mode 100644
index 000000000..0d6f1e475
--- /dev/null
+++ b/bta/le_audio/le_audio_types_test.cc
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+#include "le_audio_types.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace le_audio {
+namespace types {
+
+using ::testing::AllOf;
+using ::testing::Contains;
+using ::testing::ElementsAre;
+using ::testing::ElementsAreArray;
+using ::testing::Eq;
+using ::testing::Pair;
+using ::testing::SizeIs;
+
+TEST(LeAudioLtvMapTest, test_serialization) {
+ // clang-format off
+ const std::vector<uint8_t> ltv_test_vec{
+ 0x02, 0x01, 0x0a,
+ 0x03, 0x02, 0xaa, 0xbb,
+ 0x04, 0x03, 0xde, 0xc0, 0xde,
+ 0x05, 0x04, 0xc0, 0xde, 0xc0, 0xde,
+ };
+ // clang-format on
+
+ // Parse
+ bool success;
+ LeAudioLtvMap ltv_map =
+ LeAudioLtvMap::Parse(ltv_test_vec.data(), ltv_test_vec.size(), success);
+ ASSERT_TRUE(success);
+ ASSERT_FALSE(ltv_map.IsEmpty());
+ ASSERT_EQ((size_t)4, ltv_map.Size());
+
+ ASSERT_TRUE(ltv_map.Find(0x01));
+ ASSERT_THAT(*(ltv_map.Find(0x01)), ElementsAre(0x0a));
+ ASSERT_TRUE(ltv_map.Find(0x02));
+ ASSERT_THAT(*(ltv_map.Find(0x02)), ElementsAre(0xaa, 0xbb));
+ ASSERT_TRUE(ltv_map.Find(0x03));
+ ASSERT_THAT(*(ltv_map.Find(0x03)), ElementsAre(0xde, 0xc0, 0xde));
+ ASSERT_TRUE(ltv_map.Find(0x04));
+ ASSERT_THAT(*(ltv_map.Find(0x04)), ElementsAre(0xc0, 0xde, 0xc0, 0xde));
+
+ // RawPacket
+ std::vector<uint8_t> serialized(ltv_map.RawPacketSize());
+ ASSERT_TRUE(ltv_map.RawPacket(serialized.data()));
+ ASSERT_THAT(serialized, ElementsAreArray(ltv_test_vec));
+}
+
+TEST(LeAudioLtvMapTest, test_serialization_ltv_len_is_zero) {
+ // clang-format off
+ const std::vector<uint8_t> ltv_test_vec{
+ 0x02, 0x01, 0x0a,
+ 0x03, 0x02, 0xaa, 0xbb,
+ 0x00, 0x00, 0x00, 0x00, 0x00, // ltv_len == 0
+ 0x05, 0x04, 0xc0, 0xde, 0xc0, 0xde,
+ };
+ // clang-format on
+
+ // Parse
+ bool success;
+ LeAudioLtvMap ltv_map =
+ LeAudioLtvMap::Parse(ltv_test_vec.data(), ltv_test_vec.size(), success);
+ ASSERT_TRUE(success);
+ ASSERT_FALSE(ltv_map.IsEmpty());
+ ASSERT_EQ((size_t)3, ltv_map.Size());
+
+ ASSERT_TRUE(ltv_map.Find(0x01));
+ ASSERT_THAT(*(ltv_map.Find(0x01)), ElementsAre(0x0a));
+ ASSERT_TRUE(ltv_map.Find(0x02));
+ ASSERT_THAT(*(ltv_map.Find(0x02)), ElementsAre(0xaa, 0xbb));
+ ASSERT_TRUE(ltv_map.Find(0x04));
+ ASSERT_THAT(*(ltv_map.Find(0x04)), ElementsAre(0xc0, 0xde, 0xc0, 0xde));
+
+ // RawPacket
+ std::vector<uint8_t> serialized(ltv_map.RawPacketSize());
+ ASSERT_TRUE(ltv_map.RawPacket(serialized.data()));
+ ASSERT_THAT(serialized, ElementsAre(0x02, 0x01, 0x0a, 0x03, 0x02, 0xaa, 0xbb,
+ 0x05, 0x04, 0xc0, 0xde, 0xc0, 0xde));
+}
+
+TEST(LeAudioLtvMapTest, test_serialization_ltv_len_is_one) {
+ // clang-format off
+ const std::vector<uint8_t> ltv_test_vec{
+ 0x02, 0x01, 0x0a,
+ 0x01, 0x02,
+ };
+ // clang-format on
+
+ // Parse
+ bool success;
+ LeAudioLtvMap ltv_map =
+ LeAudioLtvMap::Parse(ltv_test_vec.data(), ltv_test_vec.size(), success);
+ ASSERT_TRUE(success);
+ ASSERT_FALSE(ltv_map.IsEmpty());
+ ASSERT_EQ((size_t)2, ltv_map.Size());
+
+ ASSERT_TRUE(ltv_map.Find(0x01));
+ ASSERT_THAT(*(ltv_map.Find(0x01)), ElementsAre(0x0a));
+ ASSERT_TRUE(ltv_map.Find(0x02));
+ ASSERT_THAT(*(ltv_map.Find(0x02)), SizeIs(0));
+
+ // RawPacket
+ std::vector<uint8_t> serialized(ltv_map.RawPacketSize());
+ ASSERT_TRUE(ltv_map.RawPacket(serialized.data()));
+ ASSERT_THAT(serialized, ElementsAreArray(ltv_test_vec));
+}
+
+TEST(LeAudioLtvMapTest, test_serialization_ltv_len_is_invalid) {
+ // clang-format off
+ const std::vector<uint8_t> ltv_test_vec_1{
+ 0x02, 0x01, 0x0a,
+ 0x04, 0x02, 0xaa, 0xbb, // one byte missing
+ };
+ const std::vector<uint8_t> ltv_test_vec_2{
+ 0x02, 0x01, 0x0a,
+ 0x03, 0x02, 0xaa, 0xbb,
+ 0x01,
+ };
+ const std::vector<uint8_t> ltv_test_vec_3{
+ 0x02, 0x01, 0x0a,
+ 0x03, 0x02, 0xaa, 0xbb,
+ 0x02, 0x03,
+ };
+ // clang-format on
+
+ // Parse
+ bool success = true;
+ LeAudioLtvMap ltv_map;
+
+ ltv_map = LeAudioLtvMap::Parse(ltv_test_vec_1.data(), ltv_test_vec_1.size(),
+ success);
+ ASSERT_FALSE(success);
+
+ ltv_map = LeAudioLtvMap::Parse(ltv_test_vec_2.data(), ltv_test_vec_2.size(),
+ success);
+ ASSERT_FALSE(success);
+
+ ltv_map = LeAudioLtvMap::Parse(ltv_test_vec_3.data(), ltv_test_vec_3.size(),
+ success);
+ ASSERT_FALSE(success);
+}
+
+} // namespace types
+} // namespace le_audio
diff --git a/bta/le_audio/mock_iso_manager.cc b/bta/le_audio/mock_iso_manager.cc
new file mode 100644
index 000000000..89579e710
--- /dev/null
+++ b/bta/le_audio/mock_iso_manager.cc
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+#include "mock_iso_manager.h"
+
+MockIsoManager* mock_pimpl_;
+MockIsoManager* MockIsoManager::GetInstance() {
+ bluetooth::hci::IsoManager::GetInstance();
+ return mock_pimpl_;
+}
+
+namespace bluetooth {
+namespace hci {
+
+struct IsoManager::impl : public MockIsoManager {
+ public:
+ impl() = default;
+ ~impl() = default;
+};
+
+IsoManager::IsoManager() {}
+
+void IsoManager::RegisterCigCallbacks(
+ iso_manager::CigCallbacks* callbacks) const {
+ if (!pimpl_) return;
+ pimpl_->RegisterCigCallbacks(callbacks);
+}
+
+void IsoManager::RegisterBigCallbacks(
+ iso_manager::BigCallbacks* callbacks) const {
+ if (!pimpl_) return;
+ pimpl_->RegisterBigCallbacks(callbacks);
+}
+
+void IsoManager::CreateCig(uint8_t cig_id,
+ struct iso_manager::cig_create_params cig_params) {
+ if (!pimpl_) return;
+ pimpl_->CreateCig(cig_id, std::move(cig_params));
+}
+
+void IsoManager::ReconfigureCig(
+ uint8_t cig_id, struct iso_manager::cig_create_params cig_params) {
+ if (!pimpl_) return;
+ pimpl_->ReconfigureCig(cig_id, std::move(cig_params));
+}
+
+void IsoManager::RemoveCig(uint8_t cig_id) { pimpl_->RemoveCig(cig_id); }
+
+void IsoManager::EstablishCis(
+ struct iso_manager::cis_establish_params conn_params) {
+ if (!pimpl_) return;
+ pimpl_->EstablishCis(std::move(conn_params));
+}
+
+void IsoManager::DisconnectCis(uint16_t cis_handle, uint8_t reason) {
+ if (!pimpl_) return;
+ pimpl_->DisconnectCis(cis_handle, reason);
+}
+
+void IsoManager::SetupIsoDataPath(
+ uint16_t iso_handle, struct iso_manager::iso_data_path_params path_params) {
+ if (!pimpl_) return;
+ pimpl_->SetupIsoDataPath(iso_handle, std::move(path_params));
+}
+
+void IsoManager::RemoveIsoDataPath(uint16_t iso_handle, uint8_t data_path_dir) {
+ if (!pimpl_) return;
+ pimpl_->RemoveIsoDataPath(iso_handle, data_path_dir);
+}
+
+void IsoManager::ReadIsoLinkQuality(uint16_t iso_handle) {
+ if (!pimpl_) return;
+ pimpl_->ReadIsoLinkQuality(iso_handle);
+}
+
+void IsoManager::SendIsoData(uint16_t iso_handle, const uint8_t* data,
+ uint16_t data_len) {
+ if (!pimpl_) return;
+ pimpl_->SendIsoData(iso_handle, data, data_len);
+}
+
+void IsoManager::CreateBig(uint8_t big_id,
+ struct iso_manager::big_create_params big_params) {
+ if (!pimpl_) return;
+ pimpl_->CreateBig(big_id, std::move(big_params));
+}
+
+void IsoManager::TerminateBig(uint8_t big_id, uint8_t reason) {
+ if (!pimpl_) return;
+ pimpl_->TerminateBig(big_id, reason);
+}
+
+void IsoManager::HandleIsoData(void* p_msg) {
+ if (!pimpl_) return;
+ pimpl_->HandleIsoData(static_cast<BT_HDR*>(p_msg));
+}
+
+void IsoManager::HandleDisconnect(uint16_t handle, uint8_t reason) {
+ if (!pimpl_) return;
+ pimpl_->HandleDisconnect(handle, reason);
+}
+
+void IsoManager::HandleNumComplDataPkts(uint8_t* p, uint8_t evt_len) {
+ if (!pimpl_) return;
+ pimpl_->HandleNumComplDataPkts(p, evt_len);
+}
+
+void IsoManager::HandleGdNumComplDataPkts(uint16_t handle, uint16_t credits) {}
+
+void IsoManager::HandleHciEvent(uint8_t sub_code, uint8_t* params,
+ uint16_t length) {
+ if (!pimpl_) return;
+ pimpl_->HandleHciEvent(sub_code, params, length);
+}
+
+void IsoManager::Start() {
+ // It is needed here as IsoManager which is a singleton creates it, but in
+ // this mock we want to destroy and recreate the mock on each test case.
+ if (!pimpl_) {
+ pimpl_ = std::make_unique<impl>();
+ }
+
+ mock_pimpl_ = pimpl_.get();
+ pimpl_->Start();
+}
+
+void IsoManager::Stop() {
+ // It is needed here as IsoManager which is a singleton creates it, but in
+ // this mock we want to destroy and recreate the mock on each test case.
+ if (pimpl_) {
+ pimpl_->Stop();
+ pimpl_.reset();
+ }
+
+ mock_pimpl_ = nullptr;
+}
+
+IsoManager::~IsoManager() = default;
+
+} // namespace hci
+} // namespace bluetooth
diff --git a/bta/le_audio/mock_iso_manager.h b/bta/le_audio/mock_iso_manager.h
new file mode 100644
index 000000000..e4a10abcc
--- /dev/null
+++ b/bta/le_audio/mock_iso_manager.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <gmock/gmock.h>
+
+#include "btm_iso_api.h"
+
+struct MockIsoManager {
+ public:
+ static MockIsoManager* GetInstance();
+
+ MockIsoManager() = default;
+ virtual ~MockIsoManager() = default;
+
+ MOCK_METHOD((void), RegisterCigCallbacks,
+ (bluetooth::hci::iso_manager::CigCallbacks * callbacks), (const));
+ MOCK_METHOD((void), RegisterBigCallbacks,
+ (bluetooth::hci::iso_manager::BigCallbacks * callbacks), (const));
+ MOCK_METHOD(
+ (void), CreateCig,
+ (uint8_t cig_id,
+ struct bluetooth::hci::iso_manager::cig_create_params cig_params));
+ MOCK_METHOD(
+ (void), ReconfigureCig,
+ (uint8_t cig_id,
+ struct bluetooth::hci::iso_manager::cig_create_params cig_params));
+ MOCK_METHOD((void), RemoveCig, (uint8_t cig_id));
+ MOCK_METHOD(
+ (void), EstablishCis,
+ (struct bluetooth::hci::iso_manager::cis_establish_params conn_params));
+ MOCK_METHOD((void), DisconnectCis, (uint16_t cis_handle, uint8_t reason));
+ MOCK_METHOD(
+ (void), SetupIsoDataPath,
+ (uint16_t iso_handle,
+ struct bluetooth::hci::iso_manager::iso_data_path_params path_params));
+ MOCK_METHOD((void), RemoveIsoDataPath,
+ (uint16_t iso_handle, uint8_t data_path_dir));
+ MOCK_METHOD((void), SendIsoData,
+ (uint16_t iso_handle, const uint8_t* data, uint16_t data_len));
+ MOCK_METHOD((void), ReadIsoLinkQuality, (uint16_t iso_handle));
+ MOCK_METHOD(
+ (void), CreateBig,
+ (uint8_t big_id,
+ struct bluetooth::hci::iso_manager::big_create_params big_params));
+ MOCK_METHOD((void), TerminateBig, (uint8_t big_id, uint8_t reason));
+ MOCK_METHOD((void), HandleIsoData, (void* p_msg));
+ MOCK_METHOD((void), HandleDisconnect, (uint16_t handle, uint8_t reason));
+ MOCK_METHOD((void), HandleNumComplDataPkts, (uint8_t * p, uint8_t evt_len));
+ MOCK_METHOD((void), HandleGdNumComplDataPkts, (uint8_t * p, uint8_t evt_len));
+ MOCK_METHOD((void), HandleHciEvent,
+ (uint8_t sub_code, uint8_t* params, uint16_t length));
+
+ MOCK_METHOD((void), Start, ());
+ MOCK_METHOD((void), Stop, ());
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(MockIsoManager);
+};
diff --git a/bta/le_audio/mock_le_audio_client_audio.cc b/bta/le_audio/mock_le_audio_client_audio.cc
new file mode 100644
index 000000000..5279b4c79
--- /dev/null
+++ b/bta/le_audio/mock_le_audio_client_audio.cc
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+#include "mock_le_audio_client_audio.h"
+
+#include <base/logging.h>
+
+/* Source mock */
+static MockLeAudioClientAudioSource* source_instance = nullptr;
+void MockLeAudioClientAudioSource::SetMockInstanceForTesting(
+ MockLeAudioClientAudioSource* mock) {
+ source_instance = mock;
+}
+
+bool LeAudioClientAudioSource::Start(
+ const LeAudioCodecConfiguration& codecConfiguration,
+ LeAudioClientAudioSinkReceiver* audioReceiver) {
+ LOG_ASSERT(source_instance)
+ << "Mock LeAudioClientAudioSource interface not set!";
+ return source_instance->Start(codecConfiguration, audioReceiver);
+}
+
+void LeAudioClientAudioSource::Stop() {
+ LOG_ASSERT(source_instance)
+ << "Mock LeAudioClientAudioSource interface not set!";
+ source_instance->Stop();
+}
+
+// FIXME: This is wrong! we will return a different class object - not even in
+// inheritance hierarchy
+const void* LeAudioClientAudioSource::Acquire() {
+ LOG_ASSERT(source_instance)
+ << "Mock LeAudioClientAudioSource interface not set!";
+ return source_instance->Acquire();
+}
+
+void LeAudioClientAudioSource::Release(const void* inst) {
+ LOG_ASSERT(source_instance)
+ << "Mock LeAudioClientAudioSource interface not set!";
+ source_instance->Release(inst);
+}
+
+void LeAudioClientAudioSource::ConfirmStreamingRequest() {
+ LOG_ASSERT(source_instance)
+ << "Mock LeAudioClientAudioSink interface not set!";
+ source_instance->ConfirmStreamingRequest();
+}
+
+void LeAudioClientAudioSource::CancelStreamingRequest() {
+ LOG_ASSERT(source_instance)
+ << "Mock LeAudioClientAudioSink interface not set!";
+ source_instance->CancelStreamingRequest();
+}
+
+void LeAudioClientAudioSource::UpdateRemoteDelay(uint16_t delay) {
+ LOG_ASSERT(source_instance)
+ << "Mock LeAudioClientAudioSource interface not set!";
+ source_instance->UpdateRemoteDelay(delay);
+}
+
+void LeAudioClientAudioSource::DebugDump(int fd) {
+ LOG_ASSERT(source_instance)
+ << "Mock LeAudioClientAudioSource interface not set!";
+ source_instance->DebugDump(fd);
+}
+
+/* Sink mock */
+static MockLeAudioClientAudioSink* sink_instance = nullptr;
+void MockLeAudioClientAudioSink::SetMockInstanceForTesting(
+ MockLeAudioClientAudioSink* mock) {
+ sink_instance = mock;
+}
+
+bool LeAudioClientAudioSink::Start(
+ const LeAudioCodecConfiguration& codecConfiguration,
+ LeAudioClientAudioSourceReceiver* audioReceiver) {
+ LOG_ASSERT(sink_instance) << "Mock LeAudioClientAudioSink interface not set!";
+ return sink_instance->Start(codecConfiguration, audioReceiver);
+}
+
+void LeAudioClientAudioSink::Stop() {
+ LOG_ASSERT(sink_instance) << "Mock LeAudioClientAudioSink interface not set!";
+ sink_instance->Stop();
+}
+
+// FIXME: This is wrong! we will return a different class object - not even in
+// inheritance hierarchy
+const void* LeAudioClientAudioSink::Acquire() {
+ LOG_ASSERT(sink_instance) << "Mock LeAudioClientAudioSink interface not set!";
+ return sink_instance->Acquire();
+}
+
+void LeAudioClientAudioSink::Release(const void* inst) {
+ LOG_ASSERT(sink_instance) << "Mock LeAudioClientAudioSink interface not set!";
+ sink_instance->Release(inst);
+}
+
+void LeAudioClientAudioSink::UpdateRemoteDelay(uint16_t delay) {
+ LOG_ASSERT(sink_instance) << "Mock LeAudioClientAudioSink interface not set!";
+ sink_instance->UpdateRemoteDelay(delay);
+}
+
+void LeAudioClientAudioSink::DebugDump(int fd) {
+ LOG_ASSERT(sink_instance) << "Mock LeAudioClientAudioSink interface not set!";
+ sink_instance->DebugDump(fd);
+}
+
+size_t LeAudioClientAudioSink::SendData(uint8_t* data, uint16_t size) {
+ LOG_ASSERT(sink_instance) << "Mock LeAudioClientAudioSink interface not set!";
+ return sink_instance->SendData(data, size);
+}
+
+void LeAudioClientAudioSink::ConfirmStreamingRequest() {
+ LOG_ASSERT(sink_instance) << "Mock LeAudioClientAudioSink interface not set!";
+ sink_instance->ConfirmStreamingRequest();
+}
+
+void LeAudioClientAudioSink::CancelStreamingRequest() {
+ LOG_ASSERT(sink_instance) << "Mock LeAudioClientAudioSink interface not set!";
+ sink_instance->CancelStreamingRequest();
+}
diff --git a/bta/le_audio/mock_le_audio_client_audio.h b/bta/le_audio/mock_le_audio_client_audio.h
new file mode 100644
index 000000000..fbae8f70c
--- /dev/null
+++ b/bta/le_audio/mock_le_audio_client_audio.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <gmock/gmock.h>
+
+#include "client_audio.h"
+
+class MockLeAudioClientAudioSource {
+ public:
+ static void SetMockInstanceForTesting(MockLeAudioClientAudioSource* mock);
+ MOCK_METHOD((bool), Start,
+ (const LeAudioCodecConfiguration& codecConfiguration,
+ LeAudioClientAudioSinkReceiver* audioReceiver));
+ MOCK_METHOD((void), Stop, ());
+ MOCK_METHOD((const void*), Acquire, ());
+ MOCK_METHOD((void), Release, (const void*));
+ MOCK_METHOD((void), ConfirmStreamingRequest, ());
+ MOCK_METHOD((void), CancelStreamingRequest, ());
+ MOCK_METHOD((void), UpdateRemoteDelay, (uint16_t delay));
+ MOCK_METHOD((void), DebugDump, (int fd));
+};
+
+class MockLeAudioClientAudioSink {
+ public:
+ static void SetMockInstanceForTesting(MockLeAudioClientAudioSink* mock);
+ MOCK_METHOD((bool), Start,
+ (const LeAudioCodecConfiguration& codecConfiguration,
+ LeAudioClientAudioSourceReceiver* audioReceiver));
+ MOCK_METHOD((void), Stop, ());
+ MOCK_METHOD((const void*), Acquire, ());
+ MOCK_METHOD((void), Release, (const void*));
+ MOCK_METHOD((size_t), SendData, (uint8_t * data, uint16_t size));
+ MOCK_METHOD((void), ConfirmStreamingRequest, ());
+ MOCK_METHOD((void), CancelStreamingRequest, ());
+ MOCK_METHOD((void), UpdateRemoteDelay, (uint16_t delay));
+ MOCK_METHOD((void), DebugDump, (int fd));
+};
diff --git a/bta/le_audio/mock_le_audio_client_audio_source.cc b/bta/le_audio/mock_le_audio_client_audio_source.cc
new file mode 100644
index 000000000..014d74be0
--- /dev/null
+++ b/bta/le_audio/mock_le_audio_client_audio_source.cc
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+#include "mock_le_audio_client_audio_source.h"
+
+static MockLeAudioClientAudioSource* instance;
+void MockLeAudioClientAudioSource::SetMockInstanceForTesting(
+ MockLeAudioClientAudioSource* mock) {
+ instance = mock;
+}
+
+bool LeAudioClientAudioSource::Start(
+ const LeAudioCodecConfiguration& codecConfiguration,
+ LeAudioClientAudioSinkReceiver* audioReceiver, uint16_t remote_delay_ms) {
+ return instance->Start(codecConfiguration, audioReceiver, remote_delay_ms);
+}
+
+void LeAudioClientAudioSource::Stop() { instance->Stop(); }
+
+// FIXME: This is wrong! we will return a different class object - not even in
+// inheritance hierarchy
+const void* LeAudioClientAudioSource::Acquire() { return instance->Acquire(); }
+
+void LeAudioClientAudioSource::Release(const void* inst) {
+ instance->Release(inst);
+}
+
+void LeAudioClientAudioSource::ConfirmStreamingRequest() {
+ instance->ConfirmStreamingRequest();
+}
+
+void LeAudioClientAudioSource::CancelStreamingRequest() {
+ instance->CancelStreamingRequest();
+}
+
+void LeAudioClientAudioSource::DebugDump(int fd) { instance->DebugDump(fd); }
diff --git a/bta/le_audio/mock_le_audio_client_audio_source.h b/bta/le_audio/mock_le_audio_client_audio_source.h
new file mode 100644
index 000000000..59646a566
--- /dev/null
+++ b/bta/le_audio/mock_le_audio_client_audio_source.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <gmock/gmock.h>
+
+#include "client_audio.h"
+
+class MockLeAudioClientAudioSource {
+ public:
+ static void SetMockInstanceForTesting(MockLeAudioClientAudioSource* mock);
+ MOCK_METHOD((bool), Start,
+ (const LeAudioCodecConfiguration& codecConfiguration,
+ LeAudioClientAudioSinkReceiver* audioReceiver,
+ uint16_t remote_delay_ms));
+ MOCK_METHOD((void), Stop, ());
+ MOCK_METHOD((const void*), Acquire, ());
+ MOCK_METHOD((void), Release, (const void*));
+ MOCK_METHOD((void), DebugDump, (int fd));
+};
diff --git a/bta/le_audio/mock_state_machine.cc b/bta/le_audio/mock_state_machine.cc
new file mode 100644
index 000000000..dbff4f427
--- /dev/null
+++ b/bta/le_audio/mock_state_machine.cc
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+#include "mock_state_machine.h"
+
+#include <base/logging.h>
+
+static MockLeAudioGroupStateMachine* mock_machine = nullptr;
+
+void le_audio::LeAudioGroupStateMachine::Initialize(
+ le_audio::LeAudioGroupStateMachine::Callbacks* state_machine_callbacks) {
+ LOG_ASSERT(mock_machine) << "Mock State Machine not set!";
+ mock_machine->Initialize(state_machine_callbacks);
+}
+
+void le_audio::LeAudioGroupStateMachine::Cleanup(void) {
+ LOG_ASSERT(mock_machine) << "Mock State Machine not set!";
+ mock_machine->Cleanup();
+}
+
+le_audio::LeAudioGroupStateMachine* le_audio::LeAudioGroupStateMachine::Get(
+ void) {
+ return mock_machine;
+}
+
+void MockLeAudioGroupStateMachine::SetMockInstanceForTesting(
+ MockLeAudioGroupStateMachine* machine) {
+ mock_machine = machine;
+}
diff --git a/bta/le_audio/mock_state_machine.h b/bta/le_audio/mock_state_machine.h
new file mode 100644
index 000000000..61a0b4f0b
--- /dev/null
+++ b/bta/le_audio/mock_state_machine.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <gmock/gmock.h>
+
+#include "state_machine.h"
+
+class MockLeAudioGroupStateMachine : public le_audio::LeAudioGroupStateMachine {
+ public:
+ MOCK_METHOD((bool), StartStream,
+ (le_audio::LeAudioDeviceGroup * group,
+ le_audio::types::LeAudioContextType context_type),
+ (override));
+ MOCK_METHOD((bool), AttachToStream,
+ (le_audio::LeAudioDeviceGroup * group,
+ le_audio::LeAudioDevice* leAudioDevice),
+ (override));
+ MOCK_METHOD((void), SuspendStream, (le_audio::LeAudioDeviceGroup * group),
+ (override));
+ MOCK_METHOD((void), StopStream, (le_audio::LeAudioDeviceGroup * group),
+ (override));
+ MOCK_METHOD((void), ProcessGattNotifEvent,
+ (uint8_t * value, uint16_t len, le_audio::types::ase* ase,
+ le_audio::LeAudioDevice* leAudioDevice,
+ le_audio::LeAudioDeviceGroup* group),
+ (override));
+
+ MOCK_METHOD((void), ProcessHciNotifOnCigCreate,
+ (le_audio::LeAudioDeviceGroup * group, uint8_t status,
+ uint8_t cig_id, std::vector<uint16_t> conn_handles),
+ (override));
+ MOCK_METHOD((void), ProcessHciNotifOnCigRemove,
+ (uint8_t status, le_audio::LeAudioDeviceGroup* group),
+ (override));
+ MOCK_METHOD(
+ (void), ProcessHciNotifCisEstablished,
+ (le_audio::LeAudioDeviceGroup * group,
+ le_audio::LeAudioDevice* leAudioDevice,
+ const bluetooth::hci::iso_manager::cis_establish_cmpl_evt* event),
+ (override));
+ MOCK_METHOD((void), ProcessHciNotifCisDisconnected,
+ (le_audio::LeAudioDeviceGroup * group,
+ le_audio::LeAudioDevice* leAudioDevice,
+ const bluetooth::hci::iso_manager::cis_disconnected_evt* event),
+ (override));
+ MOCK_METHOD((void), ProcessHciNotifSetupIsoDataPath,
+ (le_audio::LeAudioDeviceGroup * group,
+ le_audio::LeAudioDevice* leAudioDevice, uint8_t status,
+ uint16_t conn_hdl),
+ (override));
+ MOCK_METHOD((void), ProcessHciNotifRemoveIsoDataPath,
+ (le_audio::LeAudioDeviceGroup * group,
+ le_audio::LeAudioDevice* leAudioDevice, uint8_t status,
+ uint16_t conn_hdl),
+ (override));
+ MOCK_METHOD((void), Initialize,
+ (le_audio::LeAudioGroupStateMachine::Callbacks *
+ state_machine_callbacks));
+ MOCK_METHOD((void), Cleanup, ());
+ MOCK_METHOD((void), ProcessHciNotifIsoLinkQualityRead,
+ (le_audio::LeAudioDeviceGroup * group,
+ le_audio::LeAudioDevice* leAudioDevice, uint8_t conn_handle,
+ uint32_t txUnackedPackets, uint32_t txFlushedPackets,
+ uint32_t txLastSubeventPackets, uint32_t retransmittedPackets,
+ uint32_t crcErrorPackets, uint32_t rxUnreceivedPackets,
+ uint32_t duplicatePackets),
+ (override));
+ MOCK_METHOD((void), ProcessHciNotifAclDisconnected,
+ (le_audio::LeAudioDeviceGroup * group,
+ le_audio::LeAudioDevice* leAudioDevice),
+ (override));
+
+ static void SetMockInstanceForTesting(MockLeAudioGroupStateMachine* machine);
+};
diff --git a/bta/le_audio/state_machine.cc b/bta/le_audio/state_machine.cc
new file mode 100644
index 000000000..6529dcdea
--- /dev/null
+++ b/bta/le_audio/state_machine.cc
@@ -0,0 +1,1832 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com. Represented by EHIMA -
+ * www.ehima.com
+ *
+ * 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.
+ */
+
+#include "state_machine.h"
+
+#include <base/bind.h>
+#include <base/callback.h>
+
+#include <map>
+
+#include "bt_types.h"
+#include "bta_gatt_queue.h"
+#include "bta_le_audio_api.h"
+#include "btm_iso_api.h"
+#include "client_parser.h"
+#include "devices.h"
+#include "hcimsgs.h"
+#include "le_audio_types.h"
+#include "osi/include/alarm.h"
+#include "osi/include/osi.h"
+#include "osi/include/properties.h"
+
+// clang-format off
+/* ASCS state machine 1.0
+ *
+ * State machine manages group of ASEs to make transition from one state to
+ * another according to specification and keeping involved necessary externals
+ * like: ISO, CIG, ISO data path, audio path form/to upper layer.
+ *
+ * GroupStream (API): GroupStream method of this le audio implementation class
+ * object should allow transition from Idle (No Caching),
+ * Codec Configured (Caching after release) state to
+ * Streaming for all ASEs in group within time limit. Time
+ * limit should keep safe whole state machine from being
+ * stucked in any in-middle state, which is not a destination
+ * state.
+ *
+ * TODO Second functionality of streaming should be switch
+ * context which will base on previous state, context type.
+ *
+ * GroupStop (API): GroupStop method of this le audio implementation class
+ * object should allow safe transition from any state to Idle
+ * or Codec Configured (if caching supported).
+ *
+ * ╔══════════════════╦═════════════════════════════╦══════════════╦══════════════════╦══════╗
+ * ║ Current State ║ ASE Control Point Operation ║ Result ║ Next State ║ Note ║
+ * ╠══════════════════╬═════════════════════════════╬══════════════╬══════════════════╬══════╣
+ * ║ Idle ║ Config Codec ║ Success ║ Codec Configured ║ + ║
+ * ║ Codec Configured ║ Config Codec ║ Success ║ Codec Configured ║ - ║
+ * ║ Codec Configured ║ Release ║ Success ║ Releasing ║ + ║
+ * ║ Codec Configured ║ Config QoS ║ Success ║ QoS Configured ║ + ║
+ * ║ QoS Configured ║ Config Codec ║ Success ║ Codec Configured ║ - ║
+ * ║ QoS Configured ║ Config QoS ║ Success ║ QoS Configured ║ - ║
+ * ║ QoS Configured ║ Release ║ Success ║ Releasing ║ + ║
+ * ║ QoS Configured ║ Enable ║ Success ║ Enabling ║ + ║
+ * ║ Enabling ║ Release ║ Success ║ Releasing ║ + ║
+ * ║ Enabling ║ Update Metadata ║ Success ║ Enabling ║ - ║
+ * ║ Enabling ║ Disable ║ Success ║ Disabling ║ - ║
+ * ║ Enabling ║ Receiver Start Ready ║ Success ║ Streaming ║ + ║
+ * ║ Streaming ║ Update Metadata ║ Success ║ Streaming ║ - ║
+ * ║ Streaming ║ Disable ║ Success ║ Disabling ║ + ║
+ * ║ Streaming ║ Release ║ Success ║ Releasing ║ + ║
+ * ║ Disabling ║ Receiver Stop Ready ║ Success ║ QoS Configured ║ + ║
+ * ║ Disabling ║ Release ║ Success ║ Releasing ║ + ║
+ * ║ Releasing ║ Released (no caching) ║ Success ║ Idle ║ + ║
+ * ║ Releasing ║ Released (caching) ║ Success ║ Codec Configured ║ - ║
+ * ╚══════════════════╩═════════════════════════════╩══════════════╩══════════════════╩══════╝
+ *
+ * + - supported transition
+ * - - not supported
+ */
+// clang-format on
+
+using bluetooth::hci::IsoManager;
+using bluetooth::le_audio::GroupStreamStatus;
+using le_audio::LeAudioDevice;
+using le_audio::LeAudioDeviceGroup;
+using le_audio::LeAudioGroupStateMachine;
+
+using le_audio::types::ase;
+using le_audio::types::AseState;
+using le_audio::types::AudioStreamDataPathState;
+
+namespace {
+
+constexpr int linkQualityCheckInterval = 4000;
+
+static void link_quality_cb(void* data) {
+ // very ugly, but we need to pass just two bytes
+ uint16_t cis_conn_handle = *((uint16_t*)data);
+
+ IsoManager::GetInstance()->ReadIsoLinkQuality(cis_conn_handle);
+}
+
+class LeAudioGroupStateMachineImpl;
+LeAudioGroupStateMachineImpl* instance;
+
+class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
+ public:
+ LeAudioGroupStateMachineImpl(Callbacks* state_machine_callbacks_)
+ : state_machine_callbacks_(state_machine_callbacks_),
+ watchdog_(alarm_new("LeAudioStateMachineTimer")) {}
+
+ ~LeAudioGroupStateMachineImpl() {
+ alarm_free(watchdog_);
+ watchdog_ = nullptr;
+ }
+
+ bool AttachToStream(LeAudioDeviceGroup* group,
+ LeAudioDevice* leAudioDevice) override {
+ LOG(INFO) << __func__ << " group id: " << group->group_id_
+ << " device: " << leAudioDevice->address_;
+
+ /* This function is used to attach the device to the stream.
+ * Limitation here is that device should be previously in the streaming
+ * group and just got reconnected.
+ */
+ if (group->GetState() != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ LOG(ERROR) << __func__
+ << " group not in the streaming state: " << group->GetState();
+ return false;
+ }
+
+ PrepareAndSendCodecConfigure(group, leAudioDevice);
+ return true;
+ }
+
+ bool StartStream(LeAudioDeviceGroup* group,
+ le_audio::types::LeAudioContextType context_type) override {
+ LOG(INFO) << __func__ << " current state: " << group->GetState();
+
+ switch (group->GetState()) {
+ case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED:
+ case AseState::BTA_LE_AUDIO_ASE_STATE_IDLE:
+ if (!group->Configure(context_type)) {
+ LOG(ERROR) << __func__ << ", failed to set ASE configuration";
+ return false;
+ }
+
+ /* All ASEs should aim to achieve target state */
+ SetTargetState(group, AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ PrepareAndSendCodecConfigure(group, group->GetFirstActiveDevice());
+ break;
+
+ case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED: {
+ LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
+ if (!leAudioDevice) {
+ LOG(ERROR) << __func__ << ", group has no active devices";
+ return false;
+ }
+
+ /* All ASEs should aim to achieve target state */
+ SetTargetState(group, AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ PrepareAndSendEnable(leAudioDevice);
+ break;
+ }
+
+ case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING:
+ if (group->GetContextType() != context_type) {
+ /* TODO: Switch context of group */
+ group->SetContextType(context_type);
+ }
+ return true;
+
+ default:
+ LOG(ERROR) << "Unable to transit from " << group->GetState();
+ return false;
+ }
+
+ return true;
+ }
+
+ void SuspendStream(LeAudioDeviceGroup* group) override {
+ LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
+ LOG_ASSERT(leAudioDevice)
+ << __func__ << " Shouldn't be called without an active device.";
+
+ /* All ASEs should aim to achieve target state */
+ SetTargetState(group, AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+ PrepareAndSendDisable(leAudioDevice);
+ }
+
+ void StopStream(LeAudioDeviceGroup* group) override {
+ if (group->IsReleasing()) {
+ LOG(INFO) << __func__ << ", group already in releasing process";
+ return;
+ }
+
+ LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
+ if (leAudioDevice == nullptr) {
+ LOG(ERROR) << __func__
+ << " Shouldn't be called without an active device.";
+ return;
+ }
+
+ /* All Ases should aim to achieve target state */
+ SetTargetState(group, AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+ PrepareAndSendRelease(leAudioDevice);
+ }
+
+ void ProcessGattNotifEvent(uint8_t* value, uint16_t len, struct ase* ase,
+ LeAudioDevice* leAudioDevice,
+ LeAudioDeviceGroup* group) override {
+ struct le_audio::client_parser::ascs::ase_rsp_hdr arh;
+
+ ParseAseStatusHeader(arh, len, value);
+
+ LOG(INFO) << __func__ << " " << leAudioDevice->address_
+ << ", ASE id: " << +ase->id << " state changed " << ase->state
+ << " -> " << AseState(arh.state);
+
+ switch (static_cast<AseState>(arh.state)) {
+ case AseState::BTA_LE_AUDIO_ASE_STATE_IDLE:
+ AseStateMachineProcessIdle(arh, ase, group, leAudioDevice);
+ break;
+ case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED:
+ AseStateMachineProcessCodecConfigured(
+ arh, ase, value + le_audio::client_parser::ascs::kAseRspHdrMinLen,
+ len - le_audio::client_parser::ascs::kAseRspHdrMinLen, group,
+ leAudioDevice);
+ break;
+ case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
+ AseStateMachineProcessQosConfigured(arh, ase, group, leAudioDevice);
+ break;
+ case AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING:
+ AseStateMachineProcessEnabling(arh, ase, group, leAudioDevice);
+ break;
+ case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING:
+ AseStateMachineProcessStreaming(arh, ase, group, leAudioDevice);
+ break;
+ case AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING:
+ AseStateMachineProcessDisabling(arh, ase, group, leAudioDevice);
+ break;
+ case AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING:
+ AseStateMachineProcessReleasing(arh, ase, group, leAudioDevice);
+ break;
+ default:
+ LOG(ERROR) << __func__
+ << ", Wrong AES status: " << static_cast<int>(arh.state);
+ StopStream(group);
+ break;
+ }
+ }
+
+ void ProcessHciNotifOnCigCreate(LeAudioDeviceGroup* group, uint8_t status,
+ uint8_t cig_id,
+ std::vector<uint16_t> conn_handles) override {
+ uint8_t i = 0;
+ LeAudioDevice* leAudioDevice;
+ struct le_audio::types::ase* ase;
+
+ /* TODO: What if not all cises will be configured ?
+ * conn_handle.size() != active ases in group
+ */
+
+ if (!group) {
+ LOG(ERROR) << __func__ << ", invalid cig";
+ return;
+ }
+
+ if (status != HCI_SUCCESS) {
+ LOG(ERROR) << __func__
+ << ", failed to create CIG, reason: " << loghex(status);
+ StopStream(group);
+ return;
+ }
+
+ group->cig_created_ = true;
+
+ LOG(INFO) << __func__ << "Group id: " << +group->group_id_
+ << " conn_handle size " << +conn_handles.size();
+
+ /* Assign all connection handles to ases. CIS ID order is represented by the
+ * order of active ASEs in active leAudioDevices
+ */
+
+ leAudioDevice = group->GetFirstActiveDevice();
+ LOG_ASSERT(leAudioDevice)
+ << __func__ << " Shouldn't be called without an active device.";
+
+ /* Assign all connection handles to ases */
+ do {
+ ase = leAudioDevice->GetFirstActiveAseByDataPathState(
+ AudioStreamDataPathState::IDLE);
+ LOG_ASSERT(ase) << __func__
+ << " shouldn't be called without an active ASE";
+ do {
+ auto ases_pair = leAudioDevice->GetAsesByCisId(ase->cis_id);
+
+ if (ases_pair.sink) {
+ ases_pair.sink->cis_conn_hdl = conn_handles[i];
+ ases_pair.sink->data_path_state =
+ AudioStreamDataPathState::CIS_ASSIGNED;
+ }
+ if (ases_pair.source) {
+ ases_pair.source->cis_conn_hdl = conn_handles[i];
+ ases_pair.source->data_path_state =
+ AudioStreamDataPathState::CIS_ASSIGNED;
+ }
+ i++;
+ } while ((ase = leAudioDevice->GetFirstActiveAseByDataPathState(
+ AudioStreamDataPathState::IDLE)) &&
+ (i < conn_handles.size()));
+ } while ((leAudioDevice = group->GetNextActiveDevice(leAudioDevice)) &&
+ (i < conn_handles.size()));
+
+ /* Last node configured, process group to codec configured state */
+ group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+
+ if (group->GetTargetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ leAudioDevice = group->GetFirstActiveDevice();
+ LOG_ASSERT(leAudioDevice)
+ << __func__ << " Shouldn't be called without an active device.";
+ PrepareAndSendEnable(leAudioDevice);
+ } else {
+ LOG(ERROR) << __func__
+ << ", invalid state transition, from: " << group->GetState()
+ << ", to: " << group->GetTargetState();
+ StopStream(group);
+ return;
+ }
+ }
+
+ void ProcessHciNotifOnCigRemove(uint8_t status,
+ LeAudioDeviceGroup* group) override {
+ if (status) {
+ LOG(ERROR) << __func__
+ << ", failed to remove cig, id: " << loghex(group->group_id_)
+ << ", status: " << loghex(status);
+ return;
+ }
+
+ group->cig_created_ = false;
+
+ LeAudioDevice* leAudioDevice = group->GetFirstDevice();
+ if (!leAudioDevice) return;
+
+ do {
+ alarm_free(leAudioDevice->link_quality_timer);
+ leAudioDevice->link_quality_timer = nullptr;
+
+ for (auto& ase : leAudioDevice->ases_) {
+ ase.data_path_state = AudioStreamDataPathState::IDLE;
+ ase.cis_id = le_audio::kInvalidCisId;
+ }
+ } while ((leAudioDevice = group->GetNextDevice(leAudioDevice)));
+ }
+
+ void ProcessHciNotifSetupIsoDataPath(LeAudioDeviceGroup* group,
+ LeAudioDevice* leAudioDevice,
+ uint8_t status,
+ uint16_t conn_handle) override {
+ if (status) {
+ LOG(ERROR) << __func__ << ", failed to setup data path";
+ StopStream(group);
+
+ return;
+ }
+
+ /* Update state for the given cis.*/
+ auto ase = leAudioDevice->GetFirstActiveAseByDataPathState(
+ AudioStreamDataPathState::CIS_ESTABLISHED);
+
+ if (ase->cis_conn_hdl != conn_handle) {
+ LOG(ERROR) << __func__ << " Cannot find ase by handle " << +conn_handle;
+ return;
+ }
+
+ ase->data_path_state = AudioStreamDataPathState::DATA_PATH_ESTABLISHED;
+
+ if (group->GetTargetState() != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ LOG(WARNING) << __func__ << " Group " << group->group_id_
+ << " is not targeting streaming state any more";
+ return;
+ }
+
+ ase = leAudioDevice->GetNextActiveAse(ase);
+ if (!ase) {
+ leAudioDevice = group->GetNextActiveDevice(leAudioDevice);
+
+ if (!leAudioDevice) {
+ state_machine_callbacks_->StatusReportCb(group->group_id_,
+ GroupStreamStatus::STREAMING);
+ return;
+ }
+
+ ase = leAudioDevice->GetFirstActiveAse();
+ }
+
+ LOG_ASSERT(ase) << __func__ << " shouldn't be called without an active ASE";
+ if (ase->data_path_state == AudioStreamDataPathState::CIS_ESTABLISHED)
+ PrepareDataPath(ase);
+ else
+ LOG(ERROR) << __func__
+ << " CIS got disconnected? handle: " << +ase->cis_conn_hdl;
+ }
+
+ void ProcessHciNotifRemoveIsoDataPath(LeAudioDeviceGroup* group,
+ LeAudioDevice* leAudioDevice,
+ uint8_t status,
+ uint16_t conn_hdl) override {
+ if (status != HCI_SUCCESS) {
+ LOG(ERROR) << __func__ << ", failed to remove ISO data path, reason: "
+ << loghex(status);
+ StopStream(group);
+
+ return;
+ }
+
+ bool do_disconnect = false;
+
+ auto ases_pair = leAudioDevice->GetAsesByCisConnHdl(conn_hdl);
+ if (ases_pair.sink && (ases_pair.sink->data_path_state ==
+ AudioStreamDataPathState::DATA_PATH_ESTABLISHED)) {
+ ases_pair.sink->data_path_state =
+ AudioStreamDataPathState::CIS_DISCONNECTING;
+ do_disconnect = true;
+ }
+
+ if (ases_pair.source &&
+ ases_pair.source->data_path_state ==
+ AudioStreamDataPathState::DATA_PATH_ESTABLISHED) {
+ ases_pair.source->data_path_state =
+ AudioStreamDataPathState::CIS_DISCONNECTING;
+ do_disconnect = true;
+ }
+
+ if (do_disconnect)
+ IsoManager::GetInstance()->DisconnectCis(conn_hdl, HCI_ERR_PEER_USER);
+ }
+
+ void ProcessHciNotifIsoLinkQualityRead(
+ LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice,
+ uint8_t conn_handle, uint32_t txUnackedPackets, uint32_t txFlushedPackets,
+ uint32_t txLastSubeventPackets, uint32_t retransmittedPackets,
+ uint32_t crcErrorPackets, uint32_t rxUnreceivedPackets,
+ uint32_t duplicatePackets) {
+ LOG(INFO) << "conn_handle: " << loghex(conn_handle)
+ << ", txUnackedPackets: " << loghex(txUnackedPackets)
+ << ", txFlushedPackets: " << loghex(txFlushedPackets)
+ << ", txLastSubeventPackets: " << loghex(txLastSubeventPackets)
+ << ", retransmittedPackets: " << loghex(retransmittedPackets)
+ << ", crcErrorPackets: " << loghex(crcErrorPackets)
+ << ", rxUnreceivedPackets: " << loghex(rxUnreceivedPackets)
+ << ", duplicatePackets: " << loghex(duplicatePackets);
+ }
+
+ void ProcessHciNotifAclDisconnected(LeAudioDeviceGroup* group,
+ LeAudioDevice* leAudioDevice) {
+ if (leAudioDevice->link_quality_timer) {
+ alarm_free(leAudioDevice->link_quality_timer);
+ leAudioDevice->link_quality_timer = nullptr;
+ }
+
+ leAudioDevice->conn_id_ = GATT_INVALID_CONN_ID;
+
+ if (!group) {
+ LOG(ERROR) << __func__
+ << " group is null for device: " << leAudioDevice->address_
+ << " group_id: " << leAudioDevice->group_id_;
+ return;
+ }
+
+ auto* stream_conf = &group->stream_conf;
+ if (stream_conf->valid) {
+ stream_conf->sink_streams.erase(
+ std::remove_if(stream_conf->sink_streams.begin(),
+ stream_conf->sink_streams.end(),
+ [leAudioDevice](auto& pair) {
+ auto ases =
+ leAudioDevice->GetAsesByCisConnHdl(pair.first);
+ return ases.sink;
+ }),
+ stream_conf->sink_streams.end());
+
+ stream_conf->source_streams.erase(
+ std::remove_if(stream_conf->source_streams.begin(),
+ stream_conf->source_streams.end(),
+ [leAudioDevice](auto& pair) {
+ auto ases =
+ leAudioDevice->GetAsesByCisConnHdl(pair.first);
+ return ases.source;
+ }),
+ stream_conf->source_streams.end());
+
+ if (stream_conf->sink_streams.empty() &&
+ stream_conf->source_streams.empty()) {
+ LOG(INFO) << __func__ << " stream stopped ";
+ stream_conf->valid = false;
+ }
+ }
+
+ /* mark ASEs as not used. */
+ leAudioDevice->DeactivateAllAses();
+
+ DLOG(INFO) << __func__ << " device: " << leAudioDevice->address_
+ << " group connected: " << group->IsAnyDeviceConnected()
+ << " all active ase disconnected: "
+ << group->HaveAllActiveDevicesCisDisc();
+
+ /* Group has changed. Lets update available contexts */
+ group->UpdateActiveContextsMap();
+
+ /* ACL of one of the device has been dropped.
+ * If there is active CIS, do nothing here. Just update the active contexts
+ * table
+ */
+ if (group->IsAnyDeviceConnected() &&
+ !group->HaveAllActiveDevicesCisDisc()) {
+ return;
+ }
+
+ /* Group is not connected and all the CISes are down.
+ * If group is in Idle there is nothing to do here */
+ if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {
+ LOG(INFO) << __func__ << " group: " << group->group_id_ << " is in IDLE";
+ return;
+ }
+
+ /* Clean states and destroy HCI group */
+ group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+ group->SetTargetState(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+ if (alarm_is_scheduled(watchdog_)) alarm_cancel(watchdog_);
+ state_machine_callbacks_->StatusReportCb(group->group_id_,
+ GroupStreamStatus::IDLE);
+
+ if (!group->cig_created_) return;
+
+ IsoManager::GetInstance()->RemoveCig(group->group_id_);
+ }
+
+ void ProcessHciNotifCisEstablished(
+ LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice,
+ const bluetooth::hci::iso_manager::cis_establish_cmpl_evt* event)
+ override {
+ std::vector<uint8_t> value;
+
+ auto ases_pair = leAudioDevice->GetAsesByCisConnHdl(event->cis_conn_hdl);
+
+ if (event->status) {
+ if (ases_pair.sink)
+ ases_pair.sink->data_path_state =
+ AudioStreamDataPathState::CIS_ASSIGNED;
+ if (ases_pair.source)
+ ases_pair.source->data_path_state =
+ AudioStreamDataPathState::CIS_ASSIGNED;
+
+ /* CIS establishment failed. Remove CIG if no other CIS is already created
+ * or pending. If CIS is established, this will be handled in disconnected
+ * complete event
+ */
+ if (group->HaveAllActiveDevicesCisDisc()) {
+ IsoManager::GetInstance()->RemoveCig(group->group_id_);
+ }
+
+ LOG(ERROR) << __func__
+ << ", failed to create CIS, status: " << loghex(event->status);
+
+ StopStream(group);
+ return;
+ }
+
+ if (group->GetTargetState() != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ LOG(ERROR) << __func__ << ", Unintended CIS establishement event came";
+ StopStream(group);
+ return;
+ }
+
+ if (ases_pair.sink)
+ ases_pair.sink->data_path_state =
+ AudioStreamDataPathState::CIS_ESTABLISHED;
+ if (ases_pair.source)
+ ases_pair.source->data_path_state =
+ AudioStreamDataPathState::CIS_ESTABLISHED;
+
+ leAudioDevice->link_quality_timer =
+ alarm_new_periodic("le_audio_cis_link_quality");
+ leAudioDevice->link_quality_timer_data = event->cis_conn_hdl;
+ alarm_set_on_mloop(leAudioDevice->link_quality_timer,
+ linkQualityCheckInterval, link_quality_cb,
+ &leAudioDevice->link_quality_timer_data);
+
+ if (!leAudioDevice->HaveAllActiveAsesCisEst()) {
+ /* More cis established event has to come */
+ return;
+ }
+
+ std::vector<uint8_t> ids;
+
+ /* All CISes created. Send start ready for source ASE before we can go
+ * to streaming state.
+ */
+ struct ase* ase = leAudioDevice->GetFirstActiveAse();
+ LOG_ASSERT(ase) << __func__ << " shouldn't be called without an active ASE";
+ do {
+ if (ase->direction == le_audio::types::kLeAudioDirectionSource)
+ ids.push_back(ase->id);
+ } while ((ase = leAudioDevice->GetNextActiveAse(ase)));
+
+ if (ids.size() > 0) {
+ le_audio::client_parser::ascs::PrepareAseCtpAudioReceiverStartReady(
+ ids, value);
+
+ BtaGattQueue::WriteCharacteristic(leAudioDevice->conn_id_,
+ leAudioDevice->ctp_hdls_.val_hdl, value,
+ GATT_WRITE_NO_RSP, NULL, NULL);
+
+ return;
+ }
+
+ /* Cis establishment may came after setting group state to streaming, e.g.
+ * for autonomous scenario when ase is sink */
+ if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING &&
+ group->IsGroupStreamReady()) {
+ /* No more transition for group */
+ alarm_cancel(watchdog_);
+ PrepareDataPath(group);
+ }
+ }
+
+ void ProcessHciNotifCisDisconnected(
+ LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice,
+ const bluetooth::hci::iso_manager::cis_disconnected_evt* event) override {
+ /* Reset the disconnected CIS states */
+
+ alarm_free(leAudioDevice->link_quality_timer);
+ leAudioDevice->link_quality_timer = nullptr;
+ auto ases_pair = leAudioDevice->GetAsesByCisConnHdl(event->cis_conn_hdl);
+ if (ases_pair.sink) {
+ ases_pair.sink->data_path_state = AudioStreamDataPathState::CIS_ASSIGNED;
+ }
+ if (ases_pair.source) {
+ ases_pair.source->data_path_state =
+ AudioStreamDataPathState::CIS_ASSIGNED;
+ }
+
+ /* Invalidate stream configuration if needed */
+ auto* stream_conf = &group->stream_conf;
+ if (stream_conf->valid) {
+ if (ases_pair.sink) {
+ stream_conf->sink_streams.erase(
+ std::remove_if(stream_conf->sink_streams.begin(),
+ stream_conf->sink_streams.end(),
+ [&event](auto& pair) {
+ return event->cis_conn_hdl == pair.first;
+ }),
+ stream_conf->sink_streams.end());
+ }
+
+ if (ases_pair.source) {
+ stream_conf->source_streams.erase(
+ std::remove_if(stream_conf->source_streams.begin(),
+ stream_conf->source_streams.end(),
+ [&event](auto& pair) {
+ return event->cis_conn_hdl == pair.first;
+ }),
+ stream_conf->source_streams.end());
+ }
+
+ if (stream_conf->sink_streams.empty() &&
+ stream_conf->source_streams.empty()) {
+ LOG(INFO) << __func__ << " stream stopped ";
+ stream_conf->valid = false;
+ }
+ }
+
+ auto target_state = group->GetTargetState();
+ switch (target_state) {
+ case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING:
+ /* Something wrong happen when streaming or when creating stream.
+ * If there is other device connected and streaming, just leave it as it
+ * is, otherwise stop the stream.
+ */
+ if (!group->HaveAllActiveDevicesCisDisc()) {
+ /* TODO: Reconfigure LC3 codec from here or maybe other place?*/
+ return;
+ }
+
+ /*If there is no more ase to stream. Suspend audio and clear state
+ * machine -> go to Idle */
+ state_machine_callbacks_->StatusReportCb(group->group_id_,
+ GroupStreamStatus::SUSPENDED);
+ StopStream(group);
+ return;
+
+ case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
+ /* Intentional group disconnect has finished, but the last CIS in the
+ * event came after the ASE notification.
+ * If group is already suspended and all CIS are disconnected, we can
+ * report SUSPENDED state.
+ */
+ if ((group->GetState() ==
+ AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED) &&
+ group->HaveAllActiveDevicesCisDisc()) {
+ /* No more transition for group */
+ alarm_cancel(watchdog_);
+
+ state_machine_callbacks_->StatusReportCb(
+ group->group_id_, GroupStreamStatus::SUSPENDED);
+ return;
+ }
+ break;
+ case AseState::BTA_LE_AUDIO_ASE_STATE_IDLE:
+ case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED:
+ /* Those two are used when closing the stream and CIS disconnection is
+ * expected */
+ if (group->HaveAllActiveDevicesCisDisc()) {
+ IsoManager::GetInstance()->RemoveCig(group->group_id_);
+ return;
+ }
+
+ break;
+ default:
+ break;
+ }
+
+ /* We should send Receiver Stop Ready when acting as a source */
+ if (ases_pair.source &&
+ ases_pair.source->state == AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING) {
+ std::vector<uint8_t> ids = {ases_pair.source->id};
+ std::vector<uint8_t> value;
+
+ le_audio::client_parser::ascs::PrepareAseCtpAudioReceiverStopReady(ids,
+ value);
+ BtaGattQueue::WriteCharacteristic(leAudioDevice->conn_id_,
+ leAudioDevice->ctp_hdls_.val_hdl, value,
+ GATT_WRITE_NO_RSP, NULL, NULL);
+ }
+
+ /* Tear down CIS's data paths within the group */
+ struct ase* ase = leAudioDevice->GetFirstActiveAseByDataPathState(
+ AudioStreamDataPathState::DATA_PATH_ESTABLISHED);
+ if (!ase) {
+ leAudioDevice = group->GetNextActiveDevice(leAudioDevice);
+ /* No more ASEs to disconnect their CISes */
+ if (!leAudioDevice) return;
+
+ ase = leAudioDevice->GetFirstActiveAse();
+ }
+
+ LOG_ASSERT(ase) << __func__ << " shouldn't be called without an active ASE";
+ ases_pair = leAudioDevice->GetAsesByCisConnHdl(ase->cis_conn_hdl);
+
+ if (ase->data_path_state ==
+ AudioStreamDataPathState::DATA_PATH_ESTABLISHED) {
+ IsoManager::GetInstance()->RemoveIsoDataPath(
+ ase->cis_conn_hdl,
+ (ases_pair.sink
+ ? bluetooth::hci::iso_manager::kIsoDataPathDirectionOut
+ : 0x00) |
+ (ases_pair.source
+ ? bluetooth::hci::iso_manager::kIsoDataPathDirectionIn
+ : 0x00));
+ }
+ }
+
+ private:
+ static constexpr uint64_t kStateTransitionTimeoutMs = 5000;
+ static constexpr char kStateTransitionTimeoutMsProp[] =
+ "persist.bluetooth.leaudio.device.set.state.timeoutms";
+ Callbacks* state_machine_callbacks_;
+ alarm_t* watchdog_;
+
+ /* This callback is called on timeout during transition to target state */
+ void OnStateTransitionTimeout(int group_id) {
+ state_machine_callbacks_->OnStateTransitionTimeout(group_id);
+ }
+
+ void SetTargetState(LeAudioDeviceGroup* group, AseState state) {
+ group->SetTargetState(state);
+
+ /* Group should tie in time to get requested status */
+ uint64_t timeoutMs = kStateTransitionTimeoutMs;
+ timeoutMs =
+ osi_property_get_int32(kStateTransitionTimeoutMsProp, timeoutMs);
+
+ if (alarm_is_scheduled(watchdog_)) alarm_cancel(watchdog_);
+
+ alarm_set_on_mloop(
+ watchdog_, timeoutMs,
+ [](void* data) {
+ if (instance) instance->OnStateTransitionTimeout(PTR_TO_INT(data));
+ },
+ INT_TO_PTR(group->group_id_));
+ }
+
+ void CigCreate(LeAudioDeviceGroup* group) {
+ LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
+ struct ase* ase;
+ uint32_t sdu_interval_mtos, sdu_interval_stom;
+ uint8_t packing, framing, sca;
+ std::vector<EXT_CIS_CFG> cis_cfgs;
+
+ if (group->cig_created_) {
+ LOG(ERROR) << __func__ << " group id " << group->group_id_
+ << " is already created in the controller. ";
+ return;
+ }
+
+ if (!leAudioDevice) {
+ LOG(ERROR) << __func__ << ", no active devices in group";
+
+ return;
+ }
+
+ sdu_interval_mtos =
+ group->GetSduInterval(le_audio::types::kLeAudioDirectionSink);
+ sdu_interval_stom =
+ group->GetSduInterval(le_audio::types::kLeAudioDirectionSource);
+ sca = group->GetSCA();
+ packing = group->GetPacking();
+ framing = group->GetFraming();
+ uint16_t max_trans_lat_mtos = group->GetMaxTransportLatencyMtos();
+ uint16_t max_trans_lat_stom = group->GetMaxTransportLatencyStom();
+
+ do {
+ ase = leAudioDevice->GetFirstActiveAse();
+ LOG_ASSERT(ase) << __func__
+ << " shouldn't be called without an active ASE";
+ do {
+ auto& cis = ase->cis_id;
+ auto iter =
+ find_if(cis_cfgs.begin(), cis_cfgs.end(),
+ [&cis](auto const& cfg) { return cis == cfg.cis_id; });
+
+ /* CIS configuration already on list */
+ if (iter != cis_cfgs.end()) continue;
+
+ auto ases_pair = leAudioDevice->GetAsesByCisId(cis);
+ EXT_CIS_CFG cis_cfg;
+ cis_cfg.cis_id = ase->cis_id;
+ cis_cfg.phy_mtos =
+ group->GetPhyBitmask(le_audio::types::kLeAudioDirectionSink);
+ cis_cfg.phy_stom =
+ group->GetPhyBitmask(le_audio::types::kLeAudioDirectionSource);
+
+ if (ases_pair.sink) {
+ /* TODO: config should be previously adopted */
+ cis_cfg.max_sdu_size_mtos = ase->max_sdu_size;
+ cis_cfg.rtn_mtos = ase->retrans_nb;
+ }
+ if (ases_pair.source) {
+ /* TODO: config should be previously adopted */
+ cis_cfg.max_sdu_size_stom = ase->max_sdu_size;
+ cis_cfg.rtn_stom = ase->retrans_nb;
+ }
+
+ cis_cfgs.push_back(cis_cfg);
+ } while ((ase = leAudioDevice->GetNextActiveAse(ase)));
+ } while ((leAudioDevice = group->GetNextActiveDevice(leAudioDevice)));
+
+ bluetooth::hci::iso_manager::cig_create_params param = {
+ .sdu_itv_mtos = sdu_interval_mtos,
+ .sdu_itv_stom = sdu_interval_stom,
+ .sca = sca,
+ .packing = packing,
+ .framing = framing,
+ .max_trans_lat_stom = max_trans_lat_stom,
+ .max_trans_lat_mtos = max_trans_lat_mtos,
+ .cis_cfgs = std::move(cis_cfgs),
+ };
+ IsoManager::GetInstance()->CreateCig(group->group_id_, std::move(param));
+ }
+
+ static void CisCreateForDevice(LeAudioDevice* leAudioDevice) {
+ std::vector<EXT_CIS_CREATE_CFG> conn_pairs;
+ struct ase* ase = leAudioDevice->GetFirstActiveAse();
+ do {
+ /* First in ase pair is Sink, second Source */
+ auto ases_pair = leAudioDevice->GetAsesByCisConnHdl(ase->cis_conn_hdl);
+
+ /* Already in pending state - bi-directional CIS */
+ if (ase->data_path_state == AudioStreamDataPathState::CIS_PENDING)
+ continue;
+
+ if (ases_pair.sink)
+ ases_pair.sink->data_path_state = AudioStreamDataPathState::CIS_PENDING;
+ if (ases_pair.source)
+ ases_pair.source->data_path_state =
+ AudioStreamDataPathState::CIS_PENDING;
+
+ uint16_t acl_handle =
+ BTM_GetHCIConnHandle(leAudioDevice->address_, BT_TRANSPORT_LE);
+ conn_pairs.push_back({.cis_conn_handle = ase->cis_conn_hdl,
+ .acl_conn_handle = acl_handle});
+ LOG(INFO) << __func__ << " cis handle: " << +ase->cis_conn_hdl
+ << " acl handle : " << loghex(+acl_handle);
+
+ } while ((ase = leAudioDevice->GetNextActiveAse(ase)));
+
+ IsoManager::GetInstance()->EstablishCis(
+ {.conn_pairs = std::move(conn_pairs)});
+ }
+
+ static void CisCreate(LeAudioDeviceGroup* group) {
+ LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
+ struct ase* ase;
+ std::vector<EXT_CIS_CREATE_CFG> conn_pairs;
+
+ LOG_ASSERT(leAudioDevice)
+ << __func__ << " Shouldn't be called without an active device.";
+
+ do {
+ ase = leAudioDevice->GetFirstActiveAse();
+ LOG_ASSERT(ase) << __func__
+ << " shouldn't be called without an active ASE";
+ do {
+ /* First is ase pair is Sink, second Source */
+ auto ases_pair = leAudioDevice->GetAsesByCisConnHdl(ase->cis_conn_hdl);
+
+ /* Already in pending state - bi-directional CIS */
+ if (ase->data_path_state == AudioStreamDataPathState::CIS_PENDING)
+ continue;
+
+ if (ases_pair.sink)
+ ases_pair.sink->data_path_state =
+ AudioStreamDataPathState::CIS_PENDING;
+ if (ases_pair.source)
+ ases_pair.source->data_path_state =
+ AudioStreamDataPathState::CIS_PENDING;
+
+ uint16_t acl_handle =
+ BTM_GetHCIConnHandle(leAudioDevice->address_, BT_TRANSPORT_LE);
+ conn_pairs.push_back({.cis_conn_handle = ase->cis_conn_hdl,
+ .acl_conn_handle = acl_handle});
+ DLOG(INFO) << __func__ << " cis handle: " << +ase->cis_conn_hdl
+ << " acl handle : " << loghex(+acl_handle);
+
+ } while ((ase = leAudioDevice->GetNextActiveAse(ase)));
+ } while ((leAudioDevice = group->GetNextActiveDevice(leAudioDevice)));
+
+ IsoManager::GetInstance()->EstablishCis(
+ {.conn_pairs = std::move(conn_pairs)});
+ }
+
+ static void PrepareDataPath(const struct ase* ase) {
+ /* TODO Handle HW offloading as we handle here only HCI for now */
+ bluetooth::hci::iso_manager::iso_data_path_params param = {
+ .data_path_dir =
+ ase->direction == le_audio::types::kLeAudioDirectionSink
+ ? bluetooth::hci::iso_manager::kIsoDataPathDirectionIn
+ : bluetooth::hci::iso_manager::kIsoDataPathDirectionOut,
+ .data_path_id = bluetooth::hci::iso_manager::kIsoDataPathHci,
+ .codec_id_format = ase->codec_id.coding_format,
+ .codec_id_company = ase->codec_id.vendor_company_id,
+ .codec_id_vendor = ase->codec_id.vendor_codec_id,
+ .controller_delay = 0x00000000,
+ .codec_conf = std::vector<uint8_t>(),
+ };
+ IsoManager::GetInstance()->SetupIsoDataPath(ase->cis_conn_hdl,
+ std::move(param));
+ }
+
+ static inline void PrepareDataPath(LeAudioDeviceGroup* group) {
+ auto* leAudioDevice = group->GetFirstActiveDevice();
+ LOG_ASSERT(leAudioDevice)
+ << __func__ << " Shouldn't be called without an active device.";
+
+ auto* ase = leAudioDevice->GetFirstActiveAse();
+ LOG_ASSERT(ase) << __func__ << " shouldn't be called without an active ASE";
+ PrepareDataPath(ase);
+ }
+
+ static void ReleaseDataPath(LeAudioDeviceGroup* group) {
+ LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
+ LOG_ASSERT(leAudioDevice)
+ << __func__ << " Shouldn't be called without an active device.";
+
+ auto ase = leAudioDevice->GetFirstActiveAseByDataPathState(
+ AudioStreamDataPathState::DATA_PATH_ESTABLISHED);
+ LOG_ASSERT(ase) << __func__
+ << " Shouldn't be called without an active ASE.";
+
+ auto ases_pair = leAudioDevice->GetAsesByCisConnHdl(ase->cis_conn_hdl);
+
+ IsoManager::GetInstance()->RemoveIsoDataPath(
+ ase->cis_conn_hdl,
+ (ases_pair.sink ? bluetooth::hci::iso_manager::kIsoDataPathDirectionOut
+ : 0x00) |
+ (ases_pair.source
+ ? bluetooth::hci::iso_manager::kIsoDataPathDirectionIn
+ : 0x00));
+ }
+
+ void AseStateMachineProcessIdle(
+ struct le_audio::client_parser::ascs::ase_rsp_hdr& arh, struct ase* ase,
+ LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice) {
+ switch (ase->state) {
+ case AseState::BTA_LE_AUDIO_ASE_STATE_IDLE:
+ if (ase->id == 0x00) {
+ /* Initial state of Ase - update id */
+ LOG(INFO) << __func__
+ << ", discovered ase id: " << static_cast<int>(arh.id);
+ ase->id = arh.id;
+ }
+ break;
+ case AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING: {
+ LeAudioDevice* leAudioDeviceNext;
+ ase->state = AseState::BTA_LE_AUDIO_ASE_STATE_IDLE;
+ ase->active = false;
+
+ if (!leAudioDevice->HaveAllActiveAsesSameState(
+ AseState::BTA_LE_AUDIO_ASE_STATE_IDLE)) {
+ /* More ASEs notification from this device has to come for this group
+ */
+ return;
+ }
+
+ leAudioDeviceNext = group->GetNextActiveDevice(leAudioDevice);
+
+ /* Configure ASEs for next device in group */
+ if (leAudioDeviceNext) {
+ PrepareAndSendRelease(leAudioDeviceNext);
+ } else {
+ /* Last node is in releasing state*/
+ if (alarm_is_scheduled(watchdog_)) alarm_cancel(watchdog_);
+
+ group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+
+ state_machine_callbacks_->StatusReportCb(group->group_id_,
+ GroupStreamStatus::IDLE);
+ }
+ break;
+ }
+ default:
+ LOG(ERROR) << __func__ << ", invalid state transition, from: "
+ << static_cast<int>(ase->state) << ", to: "
+ << static_cast<int>(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+ StopStream(group);
+ break;
+ }
+ }
+
+ void StartConfigQoSForTheGroup(LeAudioDeviceGroup* group) {
+ LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
+ if (!leAudioDevice) {
+ LOG(ERROR) << __func__ << ", no active devices in group";
+ StopStream(group);
+ return;
+ }
+
+ PrepareAndSendConfigQos(group, leAudioDevice);
+ }
+
+ void PrepareAndSendCodecConfigure(LeAudioDeviceGroup* group,
+ LeAudioDevice* leAudioDevice) {
+ struct le_audio::client_parser::ascs::ctp_codec_conf conf;
+ std::vector<struct le_audio::client_parser::ascs::ctp_codec_conf> confs;
+ struct ase* ase;
+
+ ase = leAudioDevice->GetFirstActiveAse();
+ LOG_ASSERT(ase) << __func__ << " shouldn't be called without an active ASE";
+ do {
+ conf.ase_id = ase->id;
+ conf.target_latency = group->GetTargetLatency();
+ conf.target_phy = group->GetTargetPhy(ase->direction);
+ conf.codec_id = ase->codec_id;
+ conf.codec_config = ase->codec_config;
+ confs.push_back(conf);
+ } while ((ase = leAudioDevice->GetNextActiveAse(ase)));
+
+ std::vector<uint8_t> value;
+ le_audio::client_parser::ascs::PrepareAseCtpCodecConfig(confs, value);
+ BtaGattQueue::WriteCharacteristic(leAudioDevice->conn_id_,
+ leAudioDevice->ctp_hdls_.val_hdl, value,
+ GATT_WRITE_NO_RSP, NULL, NULL);
+ }
+
+ void AseStateMachineProcessCodecConfigured(
+ struct le_audio::client_parser::ascs::ase_rsp_hdr& arh, struct ase* ase,
+ uint8_t* data, uint16_t len, LeAudioDeviceGroup* group,
+ LeAudioDevice* leAudioDevice) {
+ if (!group) {
+ LOG(ERROR) << __func__ << ", leAudioDevice doesn't belong to any group";
+
+ return;
+ }
+
+ /* ase contain current ASE state. New state is in "arh" */
+ switch (ase->state) {
+ case AseState::BTA_LE_AUDIO_ASE_STATE_IDLE: {
+ if (ase->id == 0x00) {
+ /* Initial state of Ase - update id */
+ LOG(INFO) << __func__
+ << ", discovered ase id: " << static_cast<int>(arh.id);
+ ase->id = arh.id;
+ }
+
+ LeAudioDevice* leAudioDeviceNext;
+
+ struct le_audio::client_parser::ascs::ase_codec_configured_state_params
+ rsp;
+
+ /* Cache codec configured status values for further
+ * configuration/reconfiguration
+ */
+ if (!ParseAseStatusCodecConfiguredStateParams(rsp, len, data)) {
+ StopStream(group);
+ return;
+ }
+ ase->framing = rsp.framing;
+ ase->preferred_phy = rsp.preferred_phy;
+ ase->max_transport_latency = rsp.max_transport_latency;
+ ase->pres_delay_min = rsp.pres_delay_min;
+ ase->pres_delay_max = rsp.pres_delay_max;
+ ase->preferred_pres_delay_min = rsp.preferred_pres_delay_min;
+ ase->preferred_pres_delay_max = rsp.preferred_pres_delay_max;
+ ase->retrans_nb = rsp.preferred_retrans_nb;
+
+ ase->state = AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED;
+
+ if (group->GetTargetState() == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {
+ /* This is autonomus change of the remote device */
+ LOG(INFO) << __func__ << " Autonomus change. Just store it. ";
+ return;
+ }
+
+ if (leAudioDevice->HaveAnyUnconfiguredAses()) {
+ /* More ASEs notification from this device has to come for this group
+ */
+ return;
+ }
+
+ if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ /* We are here because of the reconnection of the single device. */
+ PrepareAndSendConfigQos(group, leAudioDevice);
+ return;
+ }
+
+ leAudioDeviceNext = group->GetNextActiveDevice(leAudioDevice);
+
+ /* Configure ASEs for next device in group */
+ if (leAudioDeviceNext) {
+ PrepareAndSendCodecConfigure(group, leAudioDeviceNext);
+ } else {
+ /* Last node configured, process group to codec configured state */
+ group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
+
+ if (group->GetTargetState() ==
+ AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ StartConfigQoSForTheGroup(group);
+ return;
+ } else {
+ LOG(ERROR) << __func__ << ", invalid state transition, from: "
+ << group->GetState()
+ << ", to: " << group->GetTargetState();
+ StopStream(group);
+ return;
+ }
+ }
+
+ break;
+ }
+ case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED: {
+ /* Received Configured in Configured state. This could be done
+ * autonomously because of the reconfiguration done by us
+ */
+
+ struct le_audio::client_parser::ascs::ase_codec_configured_state_params
+ rsp;
+
+ /* Cache codec configured status values for further
+ * configuration/reconfiguration
+ */
+ if (!ParseAseStatusCodecConfiguredStateParams(rsp, len, data)) {
+ StopStream(group);
+ return;
+ }
+
+ ase->framing = rsp.framing;
+ ase->preferred_phy = rsp.preferred_phy;
+ ase->max_transport_latency = rsp.max_transport_latency;
+ ase->pres_delay_min = rsp.pres_delay_min;
+ ase->pres_delay_max = rsp.pres_delay_max;
+ ase->preferred_pres_delay_min = rsp.preferred_pres_delay_min;
+ ase->preferred_pres_delay_max = rsp.preferred_pres_delay_max;
+ ase->retrans_nb = rsp.preferred_retrans_nb;
+
+ /* This may be a notification from a re-configured ASE */
+ ase->reconfigure = false;
+
+ if (leAudioDevice->HaveAnyUnconfiguredAses()) {
+ /* Waiting for others to be reconfigured */
+ return;
+ }
+
+ if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ /* We are here because of the reconnection of the single device. */
+ PrepareAndSendConfigQos(group, leAudioDevice);
+ return;
+ }
+
+ LeAudioDevice* leAudioDeviceNext =
+ group->GetNextActiveDevice(leAudioDevice);
+
+ /* Configure ASEs for next device in group */
+ if (leAudioDeviceNext) {
+ PrepareAndSendCodecConfigure(group, leAudioDeviceNext);
+ } else {
+ /* Last node configured, process group to codec configured state */
+ group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
+
+ if (group->GetTargetState() ==
+ AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ StartConfigQoSForTheGroup(group);
+ return;
+ } else {
+ LOG(ERROR) << __func__
+ << ", Autonomouse change ?: " << group->GetState()
+ << ", to: " << group->GetTargetState();
+ }
+ }
+
+ break;
+ }
+ case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
+ /* TODO: Config Codec */
+ break;
+ case AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING:
+ LeAudioDevice* leAudioDeviceNext;
+ ase->state = AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED;
+ ase->active = false;
+
+ if (!leAudioDevice->HaveAllActiveAsesSameState(
+ AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED)) {
+ /* More ASEs notification from this device has to come for this group
+ */
+ return;
+ }
+
+ leAudioDeviceNext = group->GetNextActiveDevice(leAudioDevice);
+
+ /* Configure ASEs for next device in group */
+ if (leAudioDeviceNext) {
+ PrepareAndSendRelease(leAudioDeviceNext);
+ } else {
+ /* Last node is in releasing state*/
+ if (alarm_is_scheduled(watchdog_)) alarm_cancel(watchdog_);
+
+ group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
+ /* Remote device has cache and keep staying in configured state after
+ * release. Therefore, we assume this is a target state requested by
+ * remote device.
+ */
+ group->SetTargetState(group->GetState());
+ state_machine_callbacks_->StatusReportCb(group->group_id_,
+ GroupStreamStatus::IDLE);
+ }
+ break;
+ default:
+ LOG(ERROR) << __func__ << ", invalid state transition, from: "
+ << static_cast<int>(ase->state) << ", to: "
+ << static_cast<int>(
+ AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+ StopStream(group);
+ break;
+ }
+ }
+
+ void AseStateMachineProcessQosConfigured(
+ struct le_audio::client_parser::ascs::ase_rsp_hdr& arh, struct ase* ase,
+ LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice) {
+ if (!group) {
+ LOG(ERROR) << __func__ << ", leAudioDevice doesn't belong to any group";
+
+ return;
+ }
+
+ switch (ase->state) {
+ case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED: {
+ ase->state = AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED;
+
+ if (!leAudioDevice->HaveAllActiveAsesSameState(
+ AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED)) {
+ /* More ASEs notification from this device has to come for this group
+ */
+ return;
+ }
+
+ if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ /* We are here because of the reconnection of the single device. */
+ PrepareAndSendEnable(leAudioDevice);
+ return;
+ }
+
+ LeAudioDevice* leAudioDeviceNext =
+ group->GetNextActiveDevice(leAudioDevice);
+
+ /* Configure ASEs qos for next device in group */
+ if (leAudioDeviceNext) {
+ PrepareAndSendConfigQos(group, leAudioDeviceNext);
+ } else {
+ CigCreate(group);
+ }
+
+ break;
+ }
+ case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
+ /* TODO: Config Codec error/Config Qos/Config QoS error/Enable error */
+ break;
+ case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING:
+ if (ase->direction == le_audio::types::kLeAudioDirectionSource) {
+ /* Source ASE cannot go from Streaming to QoS Configured state */
+ LOG(ERROR) << __func__ << ", invalid state transition, from: "
+ << static_cast<int>(ase->state) << ", to: "
+ << static_cast<int>(
+ AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+ StopStream(group);
+ return;
+ }
+
+ ase->state = AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED;
+
+ /* Process the Disable Transition of the rest of group members if no
+ * more ASE notifications has to come from this device. */
+ if (leAudioDevice->IsReadyToSuspendStream())
+ ProcessGroupDisable(group, leAudioDevice);
+
+ break;
+
+ case AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING: {
+ ase->state = AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED;
+
+ /* More ASEs notification from this device has to come for this group */
+ if (!group->HaveAllActiveDevicesAsesTheSameState(
+ AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED))
+ return;
+
+ group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+
+ if (!group->HaveAllActiveDevicesCisDisc()) return;
+
+ if (group->GetTargetState() ==
+ AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED) {
+ /* No more transition for group */
+ alarm_cancel(watchdog_);
+
+ state_machine_callbacks_->StatusReportCb(
+ group->group_id_, GroupStreamStatus::SUSPENDED);
+ } else {
+ LOG(ERROR) << __func__ << ", invalid state transition, from: "
+ << group->GetState()
+ << ", to: " << group->GetTargetState();
+ StopStream(group);
+ return;
+ }
+ break;
+ }
+ default:
+ LOG(ERROR) << __func__ << ", invalid state transition, from: "
+ << static_cast<int>(ase->state) << ", to: "
+ << static_cast<int>(
+ AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+ StopStream(group);
+ break;
+ }
+ }
+
+ void PrepareAndSendEnable(LeAudioDevice* leAudioDevice) {
+ struct le_audio::client_parser::ascs::ctp_enable conf;
+ std::vector<struct le_audio::client_parser::ascs::ctp_enable> confs;
+ std::vector<uint8_t> value;
+ struct ase* ase;
+
+ ase = leAudioDevice->GetFirstActiveAse();
+ LOG_ASSERT(ase) << __func__ << " shouldn't be called without an active ASE";
+ do {
+ conf.ase_id = ase->id;
+ conf.metadata = ase->metadata;
+ confs.push_back(conf);
+ } while ((ase = leAudioDevice->GetNextActiveAse(ase)));
+
+ le_audio::client_parser::ascs::PrepareAseCtpEnable(confs, value);
+
+ BtaGattQueue::WriteCharacteristic(leAudioDevice->conn_id_,
+ leAudioDevice->ctp_hdls_.val_hdl, value,
+ GATT_WRITE_NO_RSP, NULL, NULL);
+ }
+
+ void PrepareAndSendDisable(LeAudioDevice* leAudioDevice) {
+ ase* ase = leAudioDevice->GetFirstActiveAse();
+ LOG_ASSERT(ase) << __func__ << " shouldn't be called without an active ASE";
+
+ std::vector<uint8_t> ids;
+ do {
+ ids.push_back(ase->id);
+ } while ((ase = leAudioDevice->GetNextActiveAse(ase)));
+
+ std::vector<uint8_t> value;
+ le_audio::client_parser::ascs::PrepareAseCtpDisable(ids, value);
+
+ BtaGattQueue::WriteCharacteristic(leAudioDevice->conn_id_,
+ leAudioDevice->ctp_hdls_.val_hdl, value,
+ GATT_WRITE_NO_RSP, NULL, NULL);
+ }
+
+ void PrepareAndSendRelease(LeAudioDevice* leAudioDevice) {
+ ase* ase = leAudioDevice->GetFirstActiveAse();
+ LOG_ASSERT(ase) << __func__ << " shouldn't be called without an active ASE";
+
+ std::vector<uint8_t> ids;
+ do {
+ ids.push_back(ase->id);
+ } while ((ase = leAudioDevice->GetNextActiveAse(ase)));
+
+ std::vector<uint8_t> value;
+ le_audio::client_parser::ascs::PrepareAseCtpRelease(ids, value);
+
+ BtaGattQueue::WriteCharacteristic(leAudioDevice->conn_id_,
+ leAudioDevice->ctp_hdls_.val_hdl, value,
+ GATT_WRITE_NO_RSP, NULL, NULL);
+ }
+
+ void PrepareAndSendConfigQos(LeAudioDeviceGroup* group,
+ LeAudioDevice* leAudioDevice) {
+ std::vector<struct le_audio::client_parser::ascs::ctp_qos_conf> confs;
+
+ for (struct ase* ase = leAudioDevice->GetFirstActiveAse(); ase != nullptr;
+ ase = leAudioDevice->GetNextActiveAse(ase)) {
+ /* Get completive (to be bi-directional CIS) CIS ID for ASE */
+ uint8_t cis_id = leAudioDevice->GetMatchingBidirectionCisId(ase);
+ if (cis_id == le_audio::kInvalidCisId) {
+ /* Get next free CIS ID for group */
+ cis_id = group->GetFirstFreeCisId();
+ if (cis_id == le_audio::kInvalidCisId) {
+ LOG(ERROR) << __func__ << ", failed to get free CIS ID";
+ StopStream(group);
+ return;
+ }
+ }
+
+ ase->cis_id = cis_id;
+
+ /* TODO: Configure first ASE qos according to context type */
+ struct le_audio::client_parser::ascs::ctp_qos_conf conf;
+ conf.ase_id = ase->id;
+ conf.cig = group->group_id_;
+ conf.cis = ase->cis_id;
+ conf.framing = group->GetFraming();
+ conf.phy = group->GetPhyBitmask(ase->direction);
+ conf.max_sdu = ase->max_sdu_size;
+ conf.retrans_nb = ase->retrans_nb;
+ if (!group->GetPresentationDelay(&conf.pres_delay, ase->direction)) {
+ LOG(ERROR) << __func__ << ", inconsistent presentation delay for group";
+ StopStream(group);
+ return;
+ }
+
+ conf.sdu_interval = group->GetSduInterval(ase->direction);
+ if (!conf.sdu_interval) {
+ LOG(ERROR) << __func__ << ", unsupported SDU interval for group";
+ StopStream(group);
+ return;
+ }
+
+ if (ase->direction == le_audio::types::kLeAudioDirectionSink) {
+ conf.max_transport_latency = group->GetMaxTransportLatencyMtos();
+ } else {
+ conf.max_transport_latency = group->GetMaxTransportLatencyStom();
+ }
+ confs.push_back(conf);
+ }
+
+ LOG_ASSERT(confs.size() > 0)
+ << __func__ << " shouldn't be called without an active ASE";
+
+ std::vector<uint8_t> value;
+ le_audio::client_parser::ascs::PrepareAseCtpConfigQos(confs, value);
+
+ BtaGattQueue::WriteCharacteristic(leAudioDevice->conn_id_,
+ leAudioDevice->ctp_hdls_.val_hdl, value,
+ GATT_WRITE_NO_RSP, NULL, NULL);
+ }
+
+ void AseStateMachineProcessEnabling(
+ struct le_audio::client_parser::ascs::ase_rsp_hdr& arh, struct ase* ase,
+ LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice) {
+ if (!group) {
+ LOG(ERROR) << __func__ << ", leAudioDevice doesn't belong to any group";
+ return;
+ }
+
+ if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ /* We are here because of the reconnection of the single device. */
+ CisCreateForDevice(leAudioDevice);
+ return;
+ }
+
+ switch (ase->state) {
+ case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
+ ase->state = AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING;
+
+ if (leAudioDevice->IsReadyToCreateStream())
+ ProcessGroupEnable(group, leAudioDevice);
+
+ break;
+
+ case AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING:
+ /* Enable/Switch Content */
+ break;
+ default:
+ LOG(ERROR) << __func__ << ", invalid state transition, from: "
+ << static_cast<int>(ase->state) << ", to: "
+ << static_cast<int>(
+ AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING);
+ StopStream(group);
+ break;
+ }
+ }
+
+ void AseStateMachineProcessStreaming(
+ struct le_audio::client_parser::ascs::ase_rsp_hdr& arh, struct ase* ase,
+ LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice) {
+ if (!group) {
+ LOG(ERROR) << __func__ << ", leAudioDevice doesn't belong to any group";
+
+ return;
+ }
+
+ switch (ase->state) {
+ case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
+ /* As per ASCS 1.0 :
+ * If a CIS has been established and the server is acting as Audio Sink
+ * for the ASE, and if the server is ready to receive audio data
+ * transmitted by the client, the server may autonomously initiate the
+ * Receiver Start Ready, as defined in Section 5.4, without first
+ * sending a notification of the ASE characteristic value in the
+ * Enabling state.
+ */
+ if (ase->direction != le_audio::types::kLeAudioDirectionSink) {
+ LOG(ERROR) << __func__ << ", invalid state transition, from: "
+ << static_cast<int>(ase->state) << ", to: "
+ << static_cast<int>(
+ AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ StopStream(group);
+ return;
+ }
+
+ ase->state = AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING;
+
+ if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ /* We are here because of the reconnection of the single device. */
+ CisCreateForDevice(leAudioDevice);
+ return;
+ }
+
+ if (leAudioDevice->IsReadyToCreateStream())
+ ProcessGroupEnable(group, leAudioDevice);
+
+ break;
+
+ case AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING: {
+ std::vector<uint8_t> value;
+
+ ase->state = AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING;
+
+ if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ /* We are here because of the reconnection of the single device. */
+ auto* stream_conf = &group->stream_conf;
+ if (ase->direction == le_audio::types::kLeAudioDirectionSource) {
+ stream_conf->source_streams.emplace_back(std::make_pair(
+ ase->cis_conn_hdl, ase->codec_config.audio_channel_allocation));
+ } else {
+ stream_conf->sink_streams.emplace_back(std::make_pair(
+ ase->cis_conn_hdl, ase->codec_config.audio_channel_allocation));
+ }
+ }
+
+ if (!group->HaveAllActiveDevicesAsesTheSameState(
+ AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING)) {
+ /* More ASEs notification form this device has to come for this group
+ */
+
+ return;
+ }
+
+ if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ /* We are here because of the reconnection of the single device. */
+ return;
+ }
+
+ /* Last node is in streaming state */
+ group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+
+ /* Not all CISes establish evens came */
+ if (!group->IsGroupStreamReady()) return;
+
+ if (group->GetTargetState() ==
+ AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ /* No more transition for group */
+ alarm_cancel(watchdog_);
+ PrepareDataPath(group);
+
+ return;
+ } else {
+ LOG(ERROR) << __func__ << ", invalid state transition, from: "
+ << group->GetState()
+ << ", to: " << group->GetTargetState();
+ StopStream(group);
+ return;
+ }
+
+ break;
+ }
+ case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING:
+ /* TODO: Update metadata/Enable */
+ break;
+ default:
+ LOG(ERROR) << __func__ << ", invalid state transition, from: "
+ << static_cast<int>(ase->state) << ", to: "
+ << static_cast<int>(
+ AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ StopStream(group);
+ break;
+ }
+ }
+
+ void AseStateMachineProcessDisabling(
+ struct le_audio::client_parser::ascs::ase_rsp_hdr& arh, struct ase* ase,
+ LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice) {
+ if (!group) {
+ LOG(ERROR) << __func__ << ", leAudioDevice doesn't belong to any group";
+
+ return;
+ }
+
+ if (ase->direction == le_audio::types::kLeAudioDirectionSink) {
+ /* Sink ASE state machine does not have Disabling state */
+ LOG(ERROR) << __func__
+ << ", invalid state transition, from: " << group->GetState()
+ << ", to: " << group->GetTargetState();
+ StopStream(group);
+ return;
+ }
+
+ switch (ase->state) {
+ case AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING:
+ /* TODO: Disable */
+ break;
+ case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING:
+ ase->state = AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING;
+
+ /* Process the Disable Transition of the rest of group members if no
+ * more ASE notifications has to come from this device. */
+ if (leAudioDevice->IsReadyToSuspendStream())
+ ProcessGroupDisable(group, leAudioDevice);
+
+ break;
+
+ default:
+ LOG(ERROR) << __func__ << ", invalid state transition, from: "
+ << static_cast<int>(ase->state) << ", to: "
+ << static_cast<int>(
+ AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING);
+ StopStream(group);
+ break;
+ }
+ }
+
+ void AseStateMachineProcessReleasing(
+ struct le_audio::client_parser::ascs::ase_rsp_hdr& arh, struct ase* ase,
+ LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice) {
+ if (!group) {
+ LOG(ERROR) << __func__ << ", leAudioDevice doesn't belong to any group";
+
+ return;
+ }
+
+ switch (ase->state) {
+ case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED:
+ case AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING: {
+ ase->state = AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING;
+ break;
+ }
+ case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
+ /* At this point all of the active ASEs within group are released. */
+ if (group->cig_created_)
+ IsoManager::GetInstance()->RemoveCig(group->group_id_);
+
+ ase->state = AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING;
+ if (group->HaveAllActiveDevicesAsesTheSameState(
+ AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING))
+ group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING);
+
+ break;
+
+ case AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING:
+ case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING: {
+ ase->state = AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING;
+
+ /* Since single ase gets into Releasing state, lets assume our new
+ * target state is IDLE
+ */
+ SetTargetState(group, AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+
+ /* Happens when bi-directional completive ASE releasing state came */
+ if (ase->data_path_state == AudioStreamDataPathState::CIS_DISCONNECTING)
+ break;
+
+ if (ase->data_path_state ==
+ AudioStreamDataPathState::DATA_PATH_ESTABLISHED) {
+ auto ases_pair =
+ leAudioDevice->GetAsesByCisConnHdl(ase->cis_conn_hdl);
+ IsoManager::GetInstance()->RemoveIsoDataPath(
+ ase->cis_conn_hdl,
+ (ases_pair.sink
+ ? bluetooth::hci::iso_manager::kIsoDataPathDirectionOut
+ : 0x00) |
+ (ases_pair.source
+ ? bluetooth::hci::iso_manager::kIsoDataPathDirectionIn
+ : 0x00));
+ } else if (ase->data_path_state ==
+ AudioStreamDataPathState::CIS_ESTABLISHED ||
+ ase->data_path_state ==
+ AudioStreamDataPathState::CIS_PENDING) {
+ IsoManager::GetInstance()->DisconnectCis(ase->cis_conn_hdl,
+ HCI_ERR_PEER_USER);
+ } else {
+ DLOG(INFO) << __func__ << ", Nothing to do ase data path state: "
+ << static_cast<int>(ase->data_path_state);
+ }
+ break;
+ }
+ default:
+ LOG(ERROR) << __func__ << ", invalid state transition, from: "
+ << static_cast<int>(ase->state) << ", to: "
+ << static_cast<int>(
+ AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING);
+ break;
+ }
+ }
+
+ void ProcessGroupEnable(LeAudioDeviceGroup* group, LeAudioDevice* device) {
+ /* Enable ASEs for next device in group. */
+ LeAudioDevice* deviceNext = group->GetNextActiveDevice(device);
+ if (deviceNext) {
+ PrepareAndSendEnable(deviceNext);
+ return;
+ }
+
+ /* At this point all of the active ASEs within group are enabled. The server
+ * might perform autonomous state transition for Sink ASE and skip Enabling
+ * state notification and transit to Streaming directly. So check the group
+ * state, because we might be ready to create CIS. */
+ if (group->HaveAllActiveDevicesAsesTheSameState(
+ AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING)) {
+ group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ } else {
+ group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING);
+ }
+
+ if (group->GetTargetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ CisCreate(group);
+ } else {
+ LOG(ERROR) << __func__
+ << ", invalid state transition, from: " << group->GetState()
+ << ", to: " << group->GetTargetState();
+ StopStream(group);
+ }
+ }
+
+ void ProcessGroupDisable(LeAudioDeviceGroup* group, LeAudioDevice* device) {
+ /* Disable ASEs for next device in group. */
+ LeAudioDevice* deviceNext = group->GetNextActiveDevice(device);
+ if (deviceNext) {
+ PrepareAndSendDisable(deviceNext);
+ return;
+ }
+
+ /* At this point all of the active ASEs within group are disabled. As there
+ * is no Disabling state for Sink ASE, it might happen that all of the
+ * active ASEs are Sink ASE and will transit to QoS state. So check
+ * the group state, because we might be ready to release data path. */
+ if (group->HaveAllActiveDevicesAsesTheSameState(
+ AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED)) {
+ group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+ } else {
+ group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING);
+ }
+
+ /* Transition to QoS configured is done by CIS disconnection */
+ if (group->GetTargetState() ==
+ AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED) {
+ ReleaseDataPath(group);
+ } else {
+ LOG(ERROR) << __func__
+ << ", invalid state transition, from: " << group->GetState()
+ << ", to: " << group->GetTargetState();
+ StopStream(group);
+ }
+ }
+};
+} // namespace
+
+namespace le_audio {
+void LeAudioGroupStateMachine::Initialize(Callbacks* state_machine_callbacks_) {
+ if (instance) {
+ LOG(ERROR) << "Already initialized";
+ return;
+ }
+
+ instance = new LeAudioGroupStateMachineImpl(state_machine_callbacks_);
+}
+
+void LeAudioGroupStateMachine::Cleanup() {
+ if (!instance) return;
+
+ LeAudioGroupStateMachineImpl* ptr = instance;
+ instance = nullptr;
+
+ delete ptr;
+}
+
+LeAudioGroupStateMachine* LeAudioGroupStateMachine::Get() {
+ CHECK(instance);
+ return instance;
+}
+} // namespace le_audio
diff --git a/bta/le_audio/state_machine.h b/bta/le_audio/state_machine.h
new file mode 100644
index 000000000..21365e89e
--- /dev/null
+++ b/bta/le_audio/state_machine.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com. Represented by EHIMA -
+ * www.ehima.com
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <vector>
+
+#include "btm_iso_api.h"
+#include "btm_iso_api_types.h"
+#include "devices.h"
+#include "hardware/bt_le_audio.h"
+#include "le_audio_types.h"
+
+namespace le_audio {
+
+/* State machine interface */
+class LeAudioGroupStateMachine {
+ public:
+ class Callbacks {
+ public:
+ virtual ~Callbacks() = default;
+
+ virtual void StatusReportCb(
+ int group_id, bluetooth::le_audio::GroupStreamStatus status) = 0;
+ virtual void OnStateTransitionTimeout(int group_id) = 0;
+ };
+
+ virtual ~LeAudioGroupStateMachine() = default;
+
+ static void Initialize(Callbacks* state_machine_callbacks);
+ static void Cleanup(void);
+ static LeAudioGroupStateMachine* Get(void);
+
+ virtual bool AttachToStream(LeAudioDeviceGroup* group,
+ LeAudioDevice* leAudioDevice) = 0;
+ virtual bool StartStream(LeAudioDeviceGroup* group,
+ types::LeAudioContextType context_type) = 0;
+ virtual void SuspendStream(LeAudioDeviceGroup* group) = 0;
+ virtual void StopStream(LeAudioDeviceGroup* group) = 0;
+ virtual void ProcessGattNotifEvent(uint8_t* value, uint16_t len,
+ struct types::ase* ase,
+ LeAudioDevice* leAudioDevice,
+ LeAudioDeviceGroup* group) = 0;
+
+ virtual void ProcessHciNotifOnCigCreate(
+ LeAudioDeviceGroup* group, uint8_t status, uint8_t cig_id,
+ std::vector<uint16_t> conn_handles) = 0;
+ virtual void ProcessHciNotifOnCigRemove(uint8_t status,
+ LeAudioDeviceGroup* group) = 0;
+ virtual void ProcessHciNotifCisEstablished(
+ LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice,
+ const bluetooth::hci::iso_manager::cis_establish_cmpl_evt* event) = 0;
+ virtual void ProcessHciNotifCisDisconnected(
+ LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice,
+ const bluetooth::hci::iso_manager::cis_disconnected_evt* event) = 0;
+ virtual void ProcessHciNotifSetupIsoDataPath(LeAudioDeviceGroup* group,
+ LeAudioDevice* leAudioDevice,
+ uint8_t status,
+ uint16_t conn_hdl) = 0;
+ virtual void ProcessHciNotifRemoveIsoDataPath(LeAudioDeviceGroup* group,
+ LeAudioDevice* leAudioDevice,
+ uint8_t status,
+ uint16_t conn_hdl) = 0;
+ virtual void ProcessHciNotifIsoLinkQualityRead(
+ LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice,
+ uint8_t conn_handle, uint32_t txUnackedPackets, uint32_t txFlushedPackets,
+ uint32_t txLastSubeventPackets, uint32_t retransmittedPackets,
+ uint32_t crcErrorPackets, uint32_t rxUnreceivedPackets,
+ uint32_t duplicatePackets) = 0;
+ virtual void ProcessHciNotifAclDisconnected(LeAudioDeviceGroup* group,
+ LeAudioDevice* leAudioDevice) = 0;
+};
+} // namespace le_audio
diff --git a/bta/le_audio/state_machine_test.cc b/bta/le_audio/state_machine_test.cc
new file mode 100644
index 000000000..9bd339bc6
--- /dev/null
+++ b/bta/le_audio/state_machine_test.cc
@@ -0,0 +1,2108 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com. Represented by EHIMA -
+ * www.ehima.com
+ *
+ * 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.
+ */
+
+#include "state_machine.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <functional>
+
+#include "bta_gatt_api_mock.h"
+#include "bta_gatt_queue_mock.h"
+#include "btm_api_mock.h"
+#include "client_parser.h"
+#include "fake_osi.h"
+#include "mock_controller.h"
+#include "mock_iso_manager.h"
+#include "types/bt_transport.h"
+
+using ::testing::_;
+using ::testing::AnyNumber;
+using ::testing::AtLeast;
+using ::testing::DoAll;
+using ::testing::Invoke;
+using ::testing::Return;
+using ::testing::SaveArg;
+using ::testing::Test;
+
+std::map<std::string, int> mock_function_count_map;
+
+extern struct fake_osi_alarm_set_on_mloop fake_osi_alarm_set_on_mloop_;
+
+namespace le_audio {
+namespace internal {
+
+// Just some arbitrary initial handles - it has no real meaning
+#define ATTR_HANDLE_ASCS_POOL_START (0x0000 | 32)
+#define ATTR_HANDLE_PACS_POOL_START (0xFF00 | 64)
+
+constexpr uint16_t kContextTypeUnspecified = 0x0001;
+constexpr uint16_t kContextTypeConversational = 0x0002;
+constexpr uint16_t kContextTypeMedia = 0x0004;
+// constexpr uint16_t kContextTypeInstructional = 0x0008;
+// constexpr uint16_t kContextTypeAttentionSeeking = 0x0010;
+// constexpr uint16_t kContextTypeImmediateAllert = 0x0020;
+// constexpr uint16_t kContextTypeManMachine = 0x0040;
+// constexpr uint16_t kContextTypeEmergencyAlert = 0x0080;
+constexpr uint16_t kContextTypeRingtone = 0x0100;
+// constexpr uint16_t kContextTypeTV = 0x0200;
+// constexpr uint16_t kContextTypeRFULive = 0x0400;
+
+namespace codec_specific {
+
+constexpr uint8_t kLc3CodingFormat = 0x06;
+
+// Reference Codec Capabilities values to test against
+constexpr uint8_t kCapTypeSupportedSamplingFrequencies = 0x01;
+constexpr uint8_t kCapTypeSupportedFrameDurations = 0x02;
+constexpr uint8_t kCapTypeAudioChannelCount = 0x03;
+constexpr uint8_t kCapTypeSupportedOctetsPerCodecFrame = 0x04;
+// constexpr uint8_t kCapTypeSupportedLc3CodecFramesPerSdu = 0x05;
+
+// constexpr uint8_t kCapSamplingFrequency8000Hz = 0x0001;
+// constexpr uint8_t kCapSamplingFrequency11025Hz = 0x0002;
+constexpr uint8_t kCapSamplingFrequency16000Hz = 0x0004;
+// constexpr uint8_t kCapSamplingFrequency22050Hz = 0x0008;
+// constexpr uint8_t kCapSamplingFrequency24000Hz = 0x0010;
+// constexpr uint8_t kCapSamplingFrequency32000Hz = 0x0020;
+// constexpr uint8_t kCapSamplingFrequency44100Hz = 0x0040;
+// constexpr uint8_t kCapSamplingFrequency48000Hz = 0x0080;
+// constexpr uint8_t kCapSamplingFrequency88200Hz = 0x0100;
+// constexpr uint8_t kCapSamplingFrequency96000Hz = 0x0200;
+// constexpr uint8_t kCapSamplingFrequency176400Hz = 0x0400;
+// constexpr uint8_t kCapSamplingFrequency192000Hz = 0x0800;
+// constexpr uint8_t kCapSamplingFrequency384000Hz = 0x1000;
+
+constexpr uint8_t kCapFrameDuration7p5ms = 0x01;
+constexpr uint8_t kCapFrameDuration10ms = 0x02;
+// constexpr uint8_t kCapFrameDuration7p5msPreferred = 0x10;
+constexpr uint8_t kCapFrameDuration10msPreferred = 0x20;
+} // namespace codec_specific
+
+namespace ascs {
+constexpr uint8_t kAseStateIdle = 0x00;
+constexpr uint8_t kAseStateCodecConfigured = 0x01;
+constexpr uint8_t kAseStateQoSConfigured = 0x02;
+constexpr uint8_t kAseStateEnabling = 0x03;
+constexpr uint8_t kAseStateStreaming = 0x04;
+constexpr uint8_t kAseStateDisabling = 0x05;
+constexpr uint8_t kAseStateReleasing = 0x06;
+
+// constexpr uint8_t kAseParamDirectionServerIsAudioSink = 0x01;
+// constexpr uint8_t kAseParamDirectionServerIsAudioSource = 0x02;
+
+constexpr uint8_t kAseParamFramingUnframedSupported = 0x00;
+// constexpr uint8_t kAseParamFramingUnframedNotSupported = 0x01;
+
+// constexpr uint8_t kAseParamPreferredPhy1M = 0x01;
+// constexpr uint8_t kAseParamPreferredPhy2M = 0x02;
+// constexpr uint8_t kAseParamPreferredPhyCoded = 0x04;
+
+constexpr uint8_t kAseCtpOpcodeConfigureCodec = 0x01;
+constexpr uint8_t kAseCtpOpcodeConfigureQos = 0x02;
+constexpr uint8_t kAseCtpOpcodeEnable = 0x03;
+constexpr uint8_t kAseCtpOpcodeReceiverStartReady = 0x04;
+constexpr uint8_t kAseCtpOpcodeDisable = 0x05;
+constexpr uint8_t kAseCtpOpcodeReceiverStopReady = 0x06;
+// constexpr uint8_t kAseCtpOpcodeUpdateMetadata = 0x07;
+constexpr uint8_t kAseCtpOpcodeRelease = 0x08;
+constexpr uint8_t kAseCtpOpcodeMaxVal = kAseCtpOpcodeRelease;
+
+} // namespace ascs
+
+static RawAddress GetTestAddress(uint8_t index) {
+ return {{0xC0, 0xDE, 0xC0, 0xDE, 0x00, index}};
+}
+
+static uint8_t ase_id_last_assigned;
+
+class MockLeAudioGroupStateMachineCallbacks
+ : public LeAudioGroupStateMachine::Callbacks {
+ public:
+ MockLeAudioGroupStateMachineCallbacks() = default;
+ ~MockLeAudioGroupStateMachineCallbacks() override = default;
+ MOCK_METHOD((void), StatusReportCb,
+ (int group_id, bluetooth::le_audio::GroupStreamStatus status),
+ (override));
+ MOCK_METHOD((void), OnStateTransitionTimeout, (int group_id), (override));
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(MockLeAudioGroupStateMachineCallbacks);
+};
+
+class StateMachineTest : public Test {
+ protected:
+ void SetUp() override {
+ mock_function_count_map.clear();
+ controller::SetMockControllerInterface(&mock_controller_);
+ bluetooth::manager::SetMockBtmInterface(&btm_interface);
+ gatt::SetMockBtaGattInterface(&gatt_interface);
+ gatt::SetMockBtaGattQueue(&gatt_queue);
+
+ ase_id_last_assigned = types::ase::kAseIdInvalid;
+ LeAudioGroupStateMachine::Initialize(&mock_callbacks_);
+
+ // Support 2M Phy
+ ON_CALL(mock_controller_, SupportsBle2mPhy()).WillByDefault(Return(true));
+ ON_CALL(btm_interface, IsPhy2mSupported(_, _)).WillByDefault(Return(true));
+ ON_CALL(btm_interface, GetHCIConnHandle(_, _))
+ .WillByDefault(
+ Invoke([](RawAddress const& remote_bda, tBT_TRANSPORT transport) {
+ return remote_bda.IsEmpty()
+ ? HCI_INVALID_HANDLE
+ : ((uint16_t)(remote_bda.address[0] ^
+ remote_bda.address[1] ^
+ remote_bda.address[2]))
+ << 8 ||
+ (remote_bda.address[3] ^ remote_bda.address[4] ^
+ remote_bda.address[5]);
+ }));
+
+ ON_CALL(gatt_queue, WriteCharacteristic(_, _, _, GATT_WRITE_NO_RSP, _, _))
+ .WillByDefault(Invoke([this](uint16_t conn_id, uint16_t handle,
+ std::vector<uint8_t> value,
+ tGATT_WRITE_TYPE write_type,
+ GATT_WRITE_OP_CB cb, void* cb_data) {
+ for (auto& dev : le_audio_devices_) {
+ if (dev->conn_id_ == conn_id) {
+ // Control point write handler
+ if (dev->ctp_hdls_.val_hdl == handle) {
+ HandleCtpOperation(dev.get(), value, cb, cb_data);
+ }
+ break;
+ }
+ }
+ }));
+
+ ConfigureIsoManagerMock();
+ }
+
+ void HandleCtpOperation(LeAudioDevice* device, std::vector<uint8_t> value,
+ GATT_WRITE_OP_CB cb, void* cb_data) {
+ auto opcode = value[0];
+
+ // Verify against valid opcode range
+ ASSERT_LT(opcode, ascs::kAseCtpOpcodeMaxVal + 1);
+ ASSERT_NE(opcode, 0);
+
+ if (ase_ctp_handlers[opcode])
+ ase_ctp_handlers[opcode](device, std::move(value), cb, cb_data);
+ }
+
+/* Helper function to make a deterministic (and unique on the entire device)
+ * connection handle for a given cis.
+ */
+#define UNIQUE_CIS_CONN_HANDLE(cig_id, cis_index) (cig_id << 8 | cis_index)
+
+ void ConfigureIsoManagerMock() {
+ iso_manager_ = bluetooth::hci::IsoManager::GetInstance();
+ ASSERT_NE(iso_manager_, nullptr);
+ iso_manager_->Start();
+
+ mock_iso_manager_ = MockIsoManager::GetInstance();
+ ASSERT_NE(mock_iso_manager_, nullptr);
+
+ ON_CALL(*mock_iso_manager_, CreateCig)
+ .WillByDefault(
+ [this](uint8_t cig_id,
+ bluetooth::hci::iso_manager::cig_create_params p) {
+ DLOG(INFO) << "CreateCig";
+
+ auto& group = le_audio_device_groups_[cig_id];
+ if (group) {
+ std::vector<uint16_t> conn_handles;
+ // Fake connection ID for each cis in a request
+ for (auto i = 0u; i < p.cis_cfgs.size(); ++i) {
+ conn_handles.push_back(UNIQUE_CIS_CONN_HANDLE(cig_id, i));
+ }
+ LeAudioGroupStateMachine::Get()->ProcessHciNotifOnCigCreate(
+ group.get(), 0, cig_id, conn_handles);
+ }
+ });
+
+ ON_CALL(*mock_iso_manager_, RemoveCig)
+ .WillByDefault([this](uint8_t cig_id) {
+ DLOG(INFO) << "CreateRemove";
+
+ auto& group = le_audio_device_groups_[cig_id];
+ if (group) {
+ // Fake connection ID for each cis in a request
+ LeAudioGroupStateMachine::Get()->ProcessHciNotifOnCigRemove(
+ 0, group.get());
+ }
+ });
+
+ ON_CALL(*mock_iso_manager_, SetupIsoDataPath)
+ .WillByDefault([this](uint16_t conn_handle,
+ bluetooth::hci::iso_manager::iso_data_path_params
+ p) {
+ DLOG(INFO) << "SetupIsoDataPath";
+
+ auto dev_it =
+ std::find_if(le_audio_devices_.begin(), le_audio_devices_.end(),
+ [&conn_handle](auto& dev) {
+ auto ases = dev->GetAsesByCisConnHdl(conn_handle);
+ return (ases.sink || ases.source);
+ });
+ if (dev_it == le_audio_devices_.end()) {
+ DLOG(ERROR) << "Device not found";
+ return;
+ }
+
+ for (auto& kv_pair : le_audio_device_groups_) {
+ auto& group = kv_pair.second;
+ if (group->IsDeviceInTheGroup(dev_it->get())) {
+ LeAudioGroupStateMachine::Get()->ProcessHciNotifSetupIsoDataPath(
+ group.get(), dev_it->get(), 0, conn_handle);
+ return;
+ }
+ }
+ });
+
+ ON_CALL(*mock_iso_manager_, RemoveIsoDataPath)
+ .WillByDefault([this](uint16_t conn_handle, uint8_t iso_direction) {
+ DLOG(INFO) << "RemoveIsoDataPath";
+
+ auto dev_it =
+ std::find_if(le_audio_devices_.begin(), le_audio_devices_.end(),
+ [&conn_handle](auto& dev) {
+ auto ases = dev->GetAsesByCisConnHdl(conn_handle);
+ return (ases.sink || ases.source);
+ });
+ if (dev_it == le_audio_devices_.end()) {
+ DLOG(ERROR) << "Device not found";
+ return;
+ }
+
+ for (auto& kv_pair : le_audio_device_groups_) {
+ auto& group = kv_pair.second;
+ if (group->IsDeviceInTheGroup(dev_it->get())) {
+ LeAudioGroupStateMachine::Get()->ProcessHciNotifRemoveIsoDataPath(
+ group.get(), dev_it->get(), 0, conn_handle);
+ return;
+ }
+ }
+ });
+
+ ON_CALL(*mock_iso_manager_, EstablishCis)
+ .WillByDefault([this](bluetooth::hci::iso_manager::cis_establish_params
+ conn_params) {
+ DLOG(INFO) << "EstablishCis";
+
+ for (auto& pair : conn_params.conn_pairs) {
+ auto dev_it = std::find_if(
+ le_audio_devices_.begin(), le_audio_devices_.end(),
+ [&pair](auto& dev) {
+ auto ases = dev->GetAsesByCisConnHdl(pair.cis_conn_handle);
+ return (ases.sink || ases.source);
+ });
+ if (dev_it == le_audio_devices_.end()) {
+ DLOG(ERROR) << "Device not found";
+ return;
+ }
+
+ for (auto& kv_pair : le_audio_device_groups_) {
+ auto& group = kv_pair.second;
+ if (group->IsDeviceInTheGroup(dev_it->get())) {
+ bluetooth::hci::iso_manager::cis_establish_cmpl_evt evt;
+
+ // Fill proper values if needed
+ evt.status = 0x00;
+ evt.cig_id = group->group_id_;
+ evt.cis_conn_hdl = pair.cis_conn_handle;
+ evt.cig_sync_delay = 0;
+ evt.cis_sync_delay = 0;
+ evt.trans_lat_mtos = 0;
+ evt.trans_lat_stom = 0;
+ evt.phy_mtos = 0;
+ evt.phy_stom = 0;
+ evt.nse = 0;
+ evt.bn_mtos = 0;
+ evt.bn_stom = 0;
+ evt.ft_mtos = 0;
+ evt.ft_stom = 0;
+ evt.max_pdu_mtos = 0;
+ evt.max_pdu_stom = 0;
+ evt.iso_itv = 0;
+
+ LeAudioGroupStateMachine::Get()->ProcessHciNotifCisEstablished(
+ group.get(), dev_it->get(), &evt);
+ break;
+ }
+ }
+ }
+ });
+
+ ON_CALL(*mock_iso_manager_, DisconnectCis)
+ .WillByDefault([this](uint16_t cis_handle, uint8_t reason) {
+ DLOG(INFO) << "DisconnectCis";
+
+ auto dev_it =
+ std::find_if(le_audio_devices_.begin(), le_audio_devices_.end(),
+ [&cis_handle](auto& dev) {
+ auto ases = dev->GetAsesByCisConnHdl(cis_handle);
+ return (ases.sink || ases.source);
+ });
+ if (dev_it == le_audio_devices_.end()) {
+ DLOG(ERROR) << "Device not found";
+ return;
+ }
+
+ for (auto& kv_pair : le_audio_device_groups_) {
+ auto& group = kv_pair.second;
+ if (group->IsDeviceInTheGroup(dev_it->get())) {
+ bluetooth::hci::iso_manager::cis_disconnected_evt evt{
+ .reason = reason,
+ .cig_id = static_cast<uint8_t>(group->group_id_),
+ .cis_conn_hdl = cis_handle,
+ };
+ LeAudioGroupStateMachine::Get()->ProcessHciNotifCisDisconnected(
+ group.get(), dev_it->get(), &evt);
+ return;
+ }
+ }
+ });
+ }
+
+ void TearDown() override {
+ iso_manager_->Stop();
+ mock_iso_manager_ = nullptr;
+
+ gatt::SetMockBtaGattQueue(nullptr);
+ gatt::SetMockBtaGattInterface(nullptr);
+ bluetooth::manager::SetMockBtmInterface(nullptr);
+ controller::SetMockControllerInterface(nullptr);
+
+ for (auto i = 0u; i <= ascs::kAseCtpOpcodeMaxVal; ++i)
+ ase_ctp_handlers[i] = nullptr;
+
+ le_audio_devices_.clear();
+ cached_codec_configuration_map_.clear();
+ LeAudioGroupStateMachine::Cleanup();
+ }
+
+ std::shared_ptr<LeAudioDevice> PrepareConnectedDevice(uint8_t id,
+ bool first_connection,
+ uint8_t num_ase_snk,
+ uint8_t num_ase_src) {
+ auto leAudioDevice =
+ std::make_shared<LeAudioDevice>(GetTestAddress(id), first_connection);
+ leAudioDevice->conn_id_ = id;
+
+ uint16_t attr_handle = ATTR_HANDLE_ASCS_POOL_START;
+ leAudioDevice->snk_audio_locations_hdls_.val_hdl = attr_handle++;
+ leAudioDevice->snk_audio_locations_hdls_.ccc_hdl = attr_handle++;
+ leAudioDevice->src_audio_locations_hdls_.val_hdl = attr_handle++;
+ leAudioDevice->src_audio_locations_hdls_.ccc_hdl = attr_handle++;
+ leAudioDevice->audio_avail_hdls_.val_hdl = attr_handle++;
+ leAudioDevice->audio_avail_hdls_.ccc_hdl = attr_handle++;
+ leAudioDevice->audio_supp_cont_hdls_.val_hdl = attr_handle++;
+ leAudioDevice->audio_supp_cont_hdls_.ccc_hdl = attr_handle++;
+ leAudioDevice->ctp_hdls_.val_hdl = attr_handle++;
+ leAudioDevice->ctp_hdls_.ccc_hdl = attr_handle++;
+
+ // Add some Sink ASEs
+ while (num_ase_snk) {
+ types::ase ase(0, 0, 0x01);
+ ase.hdls.val_hdl = attr_handle++;
+ ase.hdls.ccc_hdl = attr_handle++;
+
+ leAudioDevice->ases_.emplace_back(std::move(ase));
+ num_ase_snk--;
+ }
+
+ // Add some Source ASEs
+ while (num_ase_src) {
+ types::ase ase(0, 0, 0x02);
+ ase.hdls.val_hdl = attr_handle++;
+ ase.hdls.ccc_hdl = attr_handle++;
+
+ leAudioDevice->ases_.emplace_back(std::move(ase));
+ num_ase_src--;
+ }
+
+ le_audio_devices_.push_back(leAudioDevice);
+
+ return std::move(leAudioDevice);
+ }
+
+ LeAudioDeviceGroup* GroupTheDevice(
+ int group_id, const std::shared_ptr<LeAudioDevice>& leAudioDevice) {
+ if (le_audio_device_groups_.count(group_id) == 0) {
+ le_audio_device_groups_[group_id] =
+ std::make_unique<LeAudioDeviceGroup>(group_id);
+ }
+
+ auto& group = le_audio_device_groups_[group_id];
+
+ group->AddNode(leAudioDevice);
+ if (group->IsEmpty()) return nullptr;
+
+ return &(*group);
+ }
+
+ static void InjectAseStateNotification(types::ase* ase, LeAudioDevice* device,
+ LeAudioDeviceGroup* group,
+ uint8_t new_state,
+ void* new_state_params) {
+ // Prepare additional params
+ switch (new_state) {
+ case ascs::kAseStateCodecConfigured: {
+ client_parser::ascs::ase_codec_configured_state_params* conf =
+ static_cast<
+ client_parser::ascs::ase_codec_configured_state_params*>(
+ new_state_params);
+ std::vector<uint8_t> notif_value(25 + conf->codec_spec_conf.size());
+ auto* p = notif_value.data();
+
+ UINT8_TO_STREAM(p, ase->id == types::ase::kAseIdInvalid
+ ? ++ase_id_last_assigned
+ : ase->id);
+ UINT8_TO_STREAM(p, new_state);
+
+ UINT8_TO_STREAM(p, conf->framing);
+ UINT8_TO_STREAM(p, conf->preferred_phy);
+ UINT8_TO_STREAM(p, conf->preferred_retrans_nb);
+ UINT16_TO_STREAM(p, conf->max_transport_latency);
+ UINT24_TO_STREAM(p, conf->pres_delay_min);
+ UINT24_TO_STREAM(p, conf->pres_delay_max);
+ UINT24_TO_STREAM(p, conf->preferred_pres_delay_min);
+ UINT24_TO_STREAM(p, conf->preferred_pres_delay_max);
+
+ // CodecID:
+ UINT8_TO_STREAM(p, conf->codec_id.coding_format);
+ UINT16_TO_STREAM(p, conf->codec_id.vendor_company_id);
+ UINT16_TO_STREAM(p, conf->codec_id.vendor_codec_id);
+
+ // Codec Spec. Conf. Length and Data
+ UINT8_TO_STREAM(p, conf->codec_spec_conf.size());
+ memcpy(p, conf->codec_spec_conf.data(), conf->codec_spec_conf.size());
+
+ LeAudioGroupStateMachine::Get()->ProcessGattNotifEvent(
+ notif_value.data(), notif_value.size(), ase, device, group);
+ } break;
+
+ case ascs::kAseStateQoSConfigured: {
+ client_parser::ascs::ase_qos_configured_state_params* conf =
+ static_cast<client_parser::ascs::ase_qos_configured_state_params*>(
+ new_state_params);
+ std::vector<uint8_t> notif_value(17);
+ auto* p = notif_value.data();
+
+ // Prepare header
+ UINT8_TO_STREAM(p, ase->id);
+ UINT8_TO_STREAM(p, new_state);
+
+ UINT8_TO_STREAM(p, conf->cig_id);
+ UINT8_TO_STREAM(p, conf->cis_id);
+ UINT24_TO_STREAM(p, conf->sdu_interval);
+ UINT8_TO_STREAM(p, conf->framing);
+ UINT8_TO_STREAM(p, conf->phy);
+ UINT16_TO_STREAM(p, conf->max_sdu);
+ UINT8_TO_STREAM(p, conf->retrans_nb);
+ UINT16_TO_STREAM(p, conf->max_transport_latency);
+ UINT24_TO_STREAM(p, conf->pres_delay);
+
+ LeAudioGroupStateMachine::Get()->ProcessGattNotifEvent(
+ notif_value.data(), notif_value.size(), ase, device, group);
+ } break;
+
+ case ascs::kAseStateEnabling:
+ // fall-through
+ case ascs::kAseStateStreaming:
+ // fall-through
+ case ascs::kAseStateDisabling: {
+ client_parser::ascs::ase_transient_state_params* params =
+ static_cast<client_parser::ascs::ase_transient_state_params*>(
+ new_state_params);
+ std::vector<uint8_t> notif_value(5 + params->metadata.size());
+ auto* p = notif_value.data();
+
+ // Prepare header
+ UINT8_TO_STREAM(p, ase->id);
+ UINT8_TO_STREAM(p, new_state);
+
+ UINT8_TO_STREAM(p, group->group_id_);
+ UINT8_TO_STREAM(p, ase->cis_id);
+ UINT8_TO_STREAM(p, params->metadata.size());
+ memcpy(p, params->metadata.data(), params->metadata.size());
+
+ LeAudioGroupStateMachine::Get()->ProcessGattNotifEvent(
+ notif_value.data(), notif_value.size(), ase, device, group);
+ } break;
+
+ case ascs::kAseStateReleasing:
+ // fall-through
+ case ascs::kAseStateIdle: {
+ std::vector<uint8_t> notif_value(2);
+ auto* p = notif_value.data();
+
+ // Prepare header
+ UINT8_TO_STREAM(p, ase->id == types::ase::kAseIdInvalid
+ ? ++ase_id_last_assigned
+ : ase->id);
+ UINT8_TO_STREAM(p, new_state);
+
+ LeAudioGroupStateMachine::Get()->ProcessGattNotifEvent(
+ notif_value.data(), notif_value.size(), ase, device, group);
+ } break;
+
+ default:
+ break;
+ };
+ }
+
+ static void InsertPacRecord(
+ std::vector<types::acs_ac_record>& recs,
+ uint16_t sampling_frequencies_bitfield,
+ uint8_t supported_frame_durations_bitfield,
+ uint8_t audio_channel_count_bitfield,
+ uint16_t supported_octets_per_codec_frame_min,
+ uint16_t supported_octets_per_codec_frame_max,
+ uint8_t coding_format = codec_specific::kLc3CodingFormat,
+ uint16_t vendor_company_id = 0x0000, uint16_t vendor_codec_id = 0x0000,
+ std::vector<uint8_t> metadata = {}) {
+ recs.push_back({
+ .codec_id =
+ {
+ .coding_format = coding_format,
+ .vendor_company_id = vendor_company_id,
+ .vendor_codec_id = vendor_codec_id,
+ },
+ .codec_spec_caps = types::LeAudioLtvMap({
+ {codec_specific::kCapTypeSupportedSamplingFrequencies,
+ {(uint8_t)(sampling_frequencies_bitfield),
+ (uint8_t)(sampling_frequencies_bitfield >> 8)}},
+ {codec_specific::kCapTypeSupportedFrameDurations,
+ {supported_frame_durations_bitfield}},
+ {codec_specific::kCapTypeAudioChannelCount,
+ {audio_channel_count_bitfield}},
+ {codec_specific::kCapTypeSupportedOctetsPerCodecFrame,
+ {
+ // Min
+ (uint8_t)(supported_octets_per_codec_frame_min),
+ (uint8_t)(supported_octets_per_codec_frame_min >> 8),
+ // Max
+ (uint8_t)(supported_octets_per_codec_frame_max),
+ (uint8_t)(supported_octets_per_codec_frame_max >> 8),
+ }},
+ }),
+ .metadata = std::move(metadata),
+ });
+ }
+
+ static void InjectInitialIdleNotification(LeAudioDeviceGroup* group) {
+ for (auto* device = group->GetFirstDevice(); device != nullptr;
+ device = group->GetNextDevice(device)) {
+ for (auto& ase : device->ases_) {
+ InjectAseStateNotification(&ase, device, group, ascs::kAseStateIdle,
+ nullptr);
+ }
+ }
+ }
+
+ void MultipleTestDevicePrepare(int leaudio_group_id, uint16_t context_type,
+ uint16_t device_cnt,
+ bool insert_default_pac_records = true) {
+ // Prepare fake connected device group
+ bool first_connections = true;
+ int total_devices = device_cnt;
+ le_audio::LeAudioDeviceGroup* group = nullptr;
+
+ uint8_t num_ase_snk;
+ uint8_t num_ase_src;
+ switch (context_type) {
+ case kContextTypeRingtone:
+ num_ase_snk = 1;
+ num_ase_src = 0;
+ break;
+
+ case kContextTypeMedia:
+ num_ase_snk = 2;
+ num_ase_src = 0;
+ break;
+
+ case kContextTypeConversational:
+ num_ase_snk = 1;
+ num_ase_src = 1;
+ break;
+
+ default:
+ ASSERT_TRUE(false);
+ }
+
+ while (device_cnt) {
+ auto leAudioDevice = PrepareConnectedDevice(
+ device_cnt--, first_connections, num_ase_snk, num_ase_src);
+
+ if (insert_default_pac_records) {
+ uint16_t attr_handle = ATTR_HANDLE_PACS_POOL_START;
+
+ /* As per spec, unspecified shall be supported */
+ types::AudioContexts snk_context_type = kContextTypeUnspecified;
+ types::AudioContexts src_context_type = kContextTypeUnspecified;
+
+ // Prepare Sink Published Audio Capability records
+ if ((context_type & kContextTypeRingtone) ||
+ (context_type & kContextTypeMedia) ||
+ (context_type & kContextTypeConversational)) {
+ // Set target ASE configurations
+ std::vector<types::acs_ac_record> pac_recs;
+
+ InsertPacRecord(pac_recs,
+ codec_specific::kCapSamplingFrequency16000Hz,
+ codec_specific::kCapFrameDuration10ms |
+ codec_specific::kCapFrameDuration7p5ms |
+ codec_specific::kCapFrameDuration10msPreferred,
+ 0b00000001, 30, 120);
+
+ types::hdl_pair handle_pair;
+ handle_pair.val_hdl = attr_handle++;
+ handle_pair.ccc_hdl = attr_handle++;
+
+ leAudioDevice->snk_pacs_.emplace_back(
+ std::make_tuple(std::move(handle_pair), pac_recs));
+
+ snk_context_type |= context_type;
+ leAudioDevice->snk_audio_locations_ =
+ ::le_audio::codec_spec_conf::kLeAudioLocationFrontLeft |
+ ::le_audio::codec_spec_conf::kLeAudioLocationFrontRight;
+ }
+
+ // Prepare Source Published Audio Capability records
+ if (context_type & kContextTypeConversational) {
+ // Set target ASE configurations
+ std::vector<types::acs_ac_record> pac_recs;
+
+ InsertPacRecord(pac_recs,
+ codec_specific::kCapSamplingFrequency16000Hz,
+ codec_specific::kCapFrameDuration10ms |
+ codec_specific::kCapFrameDuration7p5ms |
+ codec_specific::kCapFrameDuration10msPreferred,
+ 0b00000001, 30, 120);
+
+ types::hdl_pair handle_pair;
+ handle_pair.val_hdl = attr_handle++;
+ handle_pair.ccc_hdl = attr_handle++;
+
+ leAudioDevice->src_pacs_.emplace_back(
+ std::make_tuple(std::move(handle_pair), pac_recs));
+ src_context_type |= kContextTypeConversational;
+
+ leAudioDevice->src_audio_locations_ =
+ ::le_audio::codec_spec_conf::kLeAudioLocationFrontLeft;
+ }
+
+ leAudioDevice->SetSupportedContexts(snk_context_type, src_context_type);
+ leAudioDevice->SetAvailableContexts(snk_context_type, src_context_type);
+ }
+
+ group = GroupTheDevice(leaudio_group_id, std::move(leAudioDevice));
+ }
+
+ /* Stimulate update of active context map */
+ types::AudioContexts type_set = static_cast<uint16_t>(context_type);
+ group->UpdateActiveContextsMap(type_set);
+
+ ASSERT_NE(group, nullptr);
+ ASSERT_EQ(group->Size(), total_devices);
+ }
+
+ LeAudioDeviceGroup* PrepareSingleTestDeviceGroup(int leaudio_group_id,
+ uint16_t context_type,
+ uint16_t device_cnt = 1) {
+ MultipleTestDevicePrepare(leaudio_group_id, context_type, device_cnt);
+ return le_audio_device_groups_.count(leaudio_group_id)
+ ? le_audio_device_groups_[leaudio_group_id].get()
+ : nullptr;
+ }
+
+ void PrepareConfigureCodecHandler(LeAudioDeviceGroup* group,
+ int verify_ase_count = 0,
+ bool caching = false) {
+ ase_ctp_handlers[ascs::kAseCtpOpcodeConfigureCodec] =
+ [group, verify_ase_count, caching, this](
+ LeAudioDevice* device, std::vector<uint8_t> value,
+ GATT_WRITE_OP_CB cb, void* cb_data) {
+ auto num_ase = value[1];
+
+ // Verify ase count if needed
+ if (verify_ase_count) ASSERT_EQ(verify_ase_count, num_ase);
+
+ // Inject Configured ASE state notification for each requested ASE
+ auto* ase_p = &value[2];
+ for (auto i = 0u; i < num_ase; ++i) {
+ client_parser::ascs::ase_codec_configured_state_params
+ codec_configured_state_params;
+
+ /* Check if this is a valid ASE ID */
+ auto ase_id = *ase_p++;
+ auto it = std::find_if(
+ device->ases_.begin(), device->ases_.end(),
+ [ase_id](auto& ase) { return (ase.id == ase_id); });
+ ASSERT_NE(it, device->ases_.end());
+ const auto ase = &(*it);
+
+ // Skip target latency param
+ ase_p++;
+
+ codec_configured_state_params.preferred_phy = *ase_p++;
+ codec_configured_state_params.codec_id.coding_format = ase_p[0];
+ codec_configured_state_params.codec_id.vendor_company_id =
+ (uint16_t)(ase_p[1] << 8 | ase_p[2]),
+ codec_configured_state_params.codec_id.vendor_codec_id =
+ (uint16_t)(ase_p[3] << 8 | ase_p[4]),
+ ase_p += 5;
+
+ auto codec_spec_param_len = *ase_p++;
+ auto num_handled_bytes = ase_p - value.data();
+ codec_configured_state_params.codec_spec_conf =
+ std::vector<uint8_t>(
+ value.begin() + num_handled_bytes,
+ value.begin() + num_handled_bytes + codec_spec_param_len);
+ ase_p += codec_spec_param_len;
+
+ // Some initial QoS settings
+ codec_configured_state_params.framing =
+ ascs::kAseParamFramingUnframedSupported;
+ codec_configured_state_params.preferred_retrans_nb = 0x04;
+ codec_configured_state_params.max_transport_latency = 0x0005;
+ codec_configured_state_params.pres_delay_min = 0xABABAB;
+ codec_configured_state_params.pres_delay_max = 0xCDCDCD;
+ codec_configured_state_params.preferred_pres_delay_min =
+ types::kPresDelayNoPreference;
+ codec_configured_state_params.preferred_pres_delay_max =
+ types::kPresDelayNoPreference;
+
+ if (caching) {
+ cached_codec_configuration_map_[ase_id] =
+ codec_configured_state_params;
+ }
+ InjectAseStateNotification(ase, device, group,
+ ascs::kAseStateCodecConfigured,
+ &codec_configured_state_params);
+ }
+ };
+ }
+
+ void PrepareConfigureQosHandler(LeAudioDeviceGroup* group,
+ int verify_ase_count = 0) {
+ ase_ctp_handlers[ascs::kAseCtpOpcodeConfigureQos] =
+ [group, verify_ase_count](LeAudioDevice* device,
+ std::vector<uint8_t> value,
+ GATT_WRITE_OP_CB cb, void* cb_data) {
+ auto num_ase = value[1];
+
+ // Verify ase count if needed
+ if (verify_ase_count) ASSERT_EQ(verify_ase_count, num_ase);
+
+ // Inject Configured QoS state notification for each requested ASE
+ auto* ase_p = &value[2];
+ for (auto i = 0u; i < num_ase; ++i) {
+ client_parser::ascs::ase_qos_configured_state_params
+ qos_configured_state_params;
+
+ /* Check if this is a valid ASE ID */
+ auto ase_id = *ase_p++;
+ auto it = std::find_if(
+ device->ases_.begin(), device->ases_.end(),
+ [ase_id](auto& ase) { return (ase.id == ase_id); });
+ ASSERT_NE(it, device->ases_.end());
+ const auto ase = &(*it);
+
+ qos_configured_state_params.cig_id = *ase_p++;
+ qos_configured_state_params.cis_id = *ase_p++;
+
+ qos_configured_state_params.sdu_interval =
+ (uint32_t)((ase_p[0] << 16) | (ase_p[1] << 8) | ase_p[2]);
+ ase_p += 3;
+
+ qos_configured_state_params.framing = *ase_p++;
+ qos_configured_state_params.phy = *ase_p++;
+ qos_configured_state_params.max_sdu =
+ (uint16_t)((ase_p[0] << 8) | ase_p[1]);
+ ase_p += 2;
+
+ qos_configured_state_params.retrans_nb = *ase_p++;
+ qos_configured_state_params.max_transport_latency =
+ (uint16_t)((ase_p[0] << 8) | ase_p[1]);
+ ase_p += 2;
+
+ qos_configured_state_params.pres_delay =
+ (uint16_t)((ase_p[0] << 16) | (ase_p[1] << 8) | ase_p[2]);
+ ase_p += 3;
+
+ InjectAseStateNotification(ase, device, group,
+ ascs::kAseStateQoSConfigured,
+ &qos_configured_state_params);
+ }
+ };
+ }
+
+ void PrepareEnableHandler(LeAudioDeviceGroup* group, int verify_ase_count = 0,
+ bool inject_enabling = true) {
+ ase_ctp_handlers[ascs::kAseCtpOpcodeEnable] =
+ [group, verify_ase_count, inject_enabling](
+ LeAudioDevice* device, std::vector<uint8_t> value,
+ GATT_WRITE_OP_CB cb, void* cb_data) {
+ auto num_ase = value[1];
+
+ // Verify ase count if needed
+ if (verify_ase_count) ASSERT_EQ(verify_ase_count, num_ase);
+
+ // Inject Streaming ASE state notification for each requested ASE
+ auto* ase_p = &value[2];
+ for (auto i = 0u; i < num_ase; ++i) {
+ /* Check if this is a valid ASE ID */
+ auto ase_id = *ase_p++;
+ auto it = std::find_if(
+ device->ases_.begin(), device->ases_.end(),
+ [ase_id](auto& ase) { return (ase.id == ase_id); });
+ ASSERT_NE(it, device->ases_.end());
+ const auto ase = &(*it);
+
+ auto meta_len = *ase_p++;
+ auto num_handled_bytes = ase_p - value.data();
+ ase_p += num_handled_bytes;
+
+ client_parser::ascs::ase_transient_state_params enable_params = {
+ .metadata = std::vector<uint8_t>(
+ value.begin() + num_handled_bytes,
+ value.begin() + num_handled_bytes + meta_len)};
+
+ // Server does the 'ReceiverStartReady' on its own - goes to
+ // Streaming, when in Sink role
+ if (ase->direction & le_audio::types::kLeAudioDirectionSink) {
+ if (inject_enabling)
+ InjectAseStateNotification(ase, device, group,
+ ascs::kAseStateEnabling,
+ &enable_params);
+ InjectAseStateNotification(
+ ase, device, group, ascs::kAseStateStreaming, &enable_params);
+ } else {
+ InjectAseStateNotification(
+ ase, device, group, ascs::kAseStateEnabling, &enable_params);
+ }
+ }
+ };
+ }
+
+ void PrepareDisableHandler(LeAudioDeviceGroup* group,
+ int verify_ase_count = 0) {
+ ase_ctp_handlers[ascs::kAseCtpOpcodeDisable] =
+ [group, verify_ase_count](LeAudioDevice* device,
+ std::vector<uint8_t> value,
+ GATT_WRITE_OP_CB cb, void* cb_data) {
+ auto num_ase = value[1];
+
+ // Verify ase count if needed
+ if (verify_ase_count) ASSERT_EQ(verify_ase_count, num_ase);
+ ASSERT_EQ(value.size(), 2ul + num_ase);
+
+ // Inject Disabling & QoS Conf. ASE state notification for each ASE
+ auto* ase_p = &value[2];
+ for (auto i = 0u; i < num_ase; ++i) {
+ /* Check if this is a valid ASE ID */
+ auto ase_id = *ase_p++;
+ auto it = std::find_if(
+ device->ases_.begin(), device->ases_.end(),
+ [ase_id](auto& ase) { return (ase.id == ase_id); });
+ ASSERT_NE(it, device->ases_.end());
+ const auto ase = &(*it);
+
+ // The Disabling state is present for Source ASE
+ if (ase->direction & le_audio::types::kLeAudioDirectionSource) {
+ client_parser::ascs::ase_transient_state_params disabling_params =
+ {.metadata = {}};
+ InjectAseStateNotification(ase, device, group,
+ ascs::kAseStateDisabling,
+ &disabling_params);
+ }
+
+ // Server does the 'ReceiverStopReady' on its own - goes to
+ // Streaming, when in Sink role
+ if (ase->direction & le_audio::types::kLeAudioDirectionSink) {
+ // FIXME: For now our fake peer does not remember qos params
+ client_parser::ascs::ase_qos_configured_state_params
+ qos_configured_state_params;
+ InjectAseStateNotification(ase, device, group,
+ ascs::kAseStateQoSConfigured,
+ &qos_configured_state_params);
+ }
+ }
+ };
+ }
+
+ void PrepareReceiverStartReady(LeAudioDeviceGroup* group,
+ int verify_ase_count = 0) {
+ ase_ctp_handlers[ascs::kAseCtpOpcodeReceiverStartReady] =
+ [group, verify_ase_count](LeAudioDevice* device,
+ std::vector<uint8_t> value,
+ GATT_WRITE_OP_CB cb, void* cb_data) {
+ auto num_ase = value[1];
+
+ // Verify ase count if needed
+ if (verify_ase_count) ASSERT_EQ(verify_ase_count, num_ase);
+
+ // Inject Streaming ASE state notification for each Source ASE
+ auto* ase_p = &value[2];
+ for (auto i = 0u; i < num_ase; ++i) {
+ /* Check if this is a valid ASE ID */
+ auto ase_id = *ase_p++;
+ auto it = std::find_if(
+ device->ases_.begin(), device->ases_.end(),
+ [ase_id](auto& ase) { return (ase.id == ase_id); });
+ ASSERT_NE(it, device->ases_.end());
+
+ // Once we did the 'ReceiverStartReady' the server goes to
+ // Streaming, when in Source role
+ auto meta_len = *ase_p++;
+ auto num_handled_bytes = ase_p - value.data();
+ ase_p += num_handled_bytes;
+
+ const auto& ase = &(*it);
+ client_parser::ascs::ase_transient_state_params enable_params = {
+ .metadata = std::vector<uint8_t>(
+ value.begin() + num_handled_bytes,
+ value.begin() + num_handled_bytes + meta_len)};
+ InjectAseStateNotification(
+ ase, device, group, ascs::kAseStateStreaming, &enable_params);
+ }
+ };
+ }
+
+ void PrepareReceiverStopReady(LeAudioDeviceGroup* group,
+ int verify_ase_count = 0) {
+ ase_ctp_handlers[ascs::kAseCtpOpcodeReceiverStopReady] =
+ [group, verify_ase_count](LeAudioDevice* device,
+ std::vector<uint8_t> value,
+ GATT_WRITE_OP_CB cb, void* cb_data) {
+ auto num_ase = value[1];
+
+ // Verify ase count if needed
+ if (verify_ase_count) ASSERT_EQ(verify_ase_count, num_ase);
+
+ // Inject QoS configured ASE state notification for each Source ASE
+ auto* ase_p = &value[2];
+ for (auto i = 0u; i < num_ase; ++i) {
+ /* Check if this is a valid ASE ID */
+ auto ase_id = *ase_p++;
+ auto it = std::find_if(
+ device->ases_.begin(), device->ases_.end(),
+ [ase_id](auto& ase) { return (ase.id == ase_id); });
+ ASSERT_NE(it, device->ases_.end());
+
+ const auto& ase = &(*it);
+
+ // FIXME: For now our fake peer does not remember qos params
+ client_parser::ascs::ase_qos_configured_state_params
+ qos_configured_state_params;
+ InjectAseStateNotification(ase, device, group,
+ ascs::kAseStateQoSConfigured,
+ &qos_configured_state_params);
+ }
+ };
+ }
+
+ void PrepareReleaseHandler(LeAudioDeviceGroup* group,
+ int verify_ase_count = 0) {
+ ase_ctp_handlers[ascs::kAseCtpOpcodeRelease] =
+ [group, verify_ase_count, this](LeAudioDevice* device,
+ std::vector<uint8_t> value,
+ GATT_WRITE_OP_CB cb, void* cb_data) {
+ auto num_ase = value[1];
+
+ // Verify ase count if needed
+ if (verify_ase_count) ASSERT_EQ(verify_ase_count, num_ase);
+ ASSERT_EQ(value.size(), 2ul + num_ase);
+
+ // Inject Releasing & Idle ASE state notification for each ASE
+ auto* ase_p = &value[2];
+ for (auto i = 0u; i < num_ase; ++i) {
+ /* Check if this is a valid ASE ID */
+ auto ase_id = *ase_p++;
+ auto it = std::find_if(
+ device->ases_.begin(), device->ases_.end(),
+ [ase_id](auto& ase) { return (ase.id == ase_id); });
+ ASSERT_NE(it, device->ases_.end());
+ const auto ase = &(*it);
+
+ InjectAseStateNotification(ase, device, group,
+ ascs::kAseStateReleasing, nullptr);
+
+ /* Check if codec configuration is cached */
+ if (cached_codec_configuration_map_.count(ase_id) > 0) {
+ InjectAseStateNotification(
+ ase, device, group, ascs::kAseStateCodecConfigured,
+ &cached_codec_configuration_map_[ase_id]);
+ } else {
+ // Release - no caching
+ InjectAseStateNotification(ase, device, group,
+ ascs::kAseStateIdle, nullptr);
+ }
+ }
+ };
+ }
+
+ controller::MockControllerInterface mock_controller_;
+ bluetooth::manager::MockBtmInterface btm_interface;
+ gatt::MockBtaGattInterface gatt_interface;
+ gatt::MockBtaGattQueue gatt_queue;
+
+ bluetooth::hci::IsoManager* iso_manager_;
+ MockIsoManager* mock_iso_manager_;
+
+ std::function<void(LeAudioDevice* device, std::vector<uint8_t> value,
+ GATT_WRITE_OP_CB cb, void* cb_data)>
+ ase_ctp_handlers[ascs::kAseCtpOpcodeMaxVal + 1] = {nullptr};
+ std::map<int, client_parser::ascs::ase_codec_configured_state_params>
+ cached_codec_configuration_map_;
+
+ MockLeAudioGroupStateMachineCallbacks mock_callbacks_;
+ std::vector<std::shared_ptr<LeAudioDevice>> le_audio_devices_;
+ std::map<uint8_t, std::unique_ptr<LeAudioDeviceGroup>>
+ le_audio_device_groups_;
+};
+
+TEST_F(StateMachineTest, testInit) {
+ ASSERT_NE(LeAudioGroupStateMachine::Get(), nullptr);
+}
+
+TEST_F(StateMachineTest, testCleanup) {
+ ASSERT_NE(LeAudioGroupStateMachine::Get(), nullptr);
+ LeAudioGroupStateMachine::Cleanup();
+ EXPECT_DEATH(LeAudioGroupStateMachine::Get(), "");
+}
+
+TEST_F(StateMachineTest, testConfigureCodecSingle) {
+ const auto context_type = kContextTypeRingtone;
+ const int leaudio_group_id = 2;
+
+ // Prepare fake connected device group
+ auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type);
+
+ /* Since we prepared device with Ringtone context in mind, only one ASE
+ * should have been configured.
+ */
+ auto* leAudioDevice = group->GetFirstDevice();
+ PrepareConfigureCodecHandler(group, 1);
+
+ // Start the configuration and stream Media content
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(2);
+
+ InjectInitialIdleNotification(group);
+
+ ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type)));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
+}
+
+TEST_F(StateMachineTest, testConfigureCodecMulti) {
+ const auto context_type = kContextTypeMedia;
+ const auto leaudio_group_id = 2;
+ const auto num_devices = 2;
+
+ // Prepare multiple fake connected devices in a group
+ auto* group =
+ PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices);
+ ASSERT_EQ(group->Size(), num_devices);
+
+ PrepareConfigureCodecHandler(group);
+
+ auto expected_devices_written = 0;
+ auto* leAudioDevice = group->GetFirstDevice();
+ while (leAudioDevice) {
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(leAudioDevice->conn_id_,
+ leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(AtLeast(1));
+ expected_devices_written++;
+ leAudioDevice = group->GetNextDevice(leAudioDevice);
+ }
+ ASSERT_EQ(expected_devices_written, num_devices);
+
+ InjectInitialIdleNotification(group);
+
+ // Start the configuration and stream the content
+ ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type)));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
+}
+
+TEST_F(StateMachineTest, testConfigureQosSingle) {
+ const auto context_type = kContextTypeRingtone;
+ const int leaudio_group_id = 3;
+
+ // Prepare fake connected device group
+ auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type);
+
+ /* Since we prepared device with Ringtone context in mind, only one ASE
+ * should have been configured.
+ */
+ auto* leAudioDevice = group->GetFirstDevice();
+ PrepareConfigureCodecHandler(group, 1);
+ PrepareConfigureQosHandler(group, 1);
+
+ // Start the configuration and stream Media content
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(3);
+
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0);
+
+ InjectInitialIdleNotification(group);
+
+ ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type)));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+}
+
+TEST_F(StateMachineTest, testConfigureQosMultiple) {
+ const auto context_type = kContextTypeMedia;
+ const auto leaudio_group_id = 3;
+ const auto num_devices = 2;
+
+ // Prepare multiple fake connected devices in a group
+ auto* group =
+ PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices);
+ ASSERT_EQ(group->Size(), num_devices);
+
+ PrepareConfigureCodecHandler(group);
+ PrepareConfigureQosHandler(group);
+
+ auto* leAudioDevice = group->GetFirstDevice();
+ auto expected_devices_written = 0;
+ while (leAudioDevice) {
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(leAudioDevice->conn_id_,
+ leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(AtLeast(2));
+ expected_devices_written++;
+ leAudioDevice = group->GetNextDevice(leAudioDevice);
+ }
+ ASSERT_EQ(expected_devices_written, num_devices);
+
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0);
+
+ InjectInitialIdleNotification(group);
+
+ // Start the configuration and stream Media content
+ ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type)));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+}
+
+TEST_F(StateMachineTest, testStreamSingle) {
+ const auto context_type = kContextTypeRingtone;
+ const int leaudio_group_id = 4;
+
+ // Prepare fake connected device group
+ auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type);
+
+ /* Since we prepared device with Ringtone context in mind, only one ASE
+ * should have been configured.
+ */
+ PrepareConfigureCodecHandler(group, 1);
+ PrepareConfigureQosHandler(group, 1);
+ PrepareEnableHandler(group, 1);
+
+ auto* leAudioDevice = group->GetFirstDevice();
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(3);
+
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0);
+
+ InjectInitialIdleNotification(group);
+
+ // Validate GroupStreamStatus
+ EXPECT_CALL(
+ mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::STREAMING));
+
+ // Start the configuration and stream Media content
+ ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type)));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+}
+
+TEST_F(StateMachineTest, testStreamSkipEnablingSink) {
+ const auto context_type = kContextTypeRingtone;
+ const int leaudio_group_id = 4;
+
+ // Prepare fake connected device group
+ auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type);
+
+ /* Since we prepared device with Ringtone context in mind, only one ASE
+ * should have been configured.
+ */
+ PrepareConfigureCodecHandler(group, 1);
+ PrepareConfigureQosHandler(group, 1);
+ PrepareEnableHandler(group, 1, false);
+
+ auto* leAudioDevice = group->GetFirstDevice();
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(3);
+
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0);
+
+ InjectInitialIdleNotification(group);
+
+ // Validate GroupStreamStatus
+ EXPECT_CALL(
+ mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::STREAMING));
+
+ // Start the configuration and stream Media content
+ ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type)));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+}
+
+TEST_F(StateMachineTest, testStreamSkipEnablingSinkSource) {
+ const auto context_type = kContextTypeConversational;
+ const int leaudio_group_id = 4;
+
+ // Prepare fake connected device group
+ auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type);
+
+ /* Since we prepared device with Conversional context in mind, one Sink ASE
+ * and one Source ASE should have been configured.
+ */
+ PrepareConfigureCodecHandler(group, 2);
+ PrepareConfigureQosHandler(group, 2);
+ PrepareEnableHandler(group, 2, false);
+ PrepareReceiverStartReady(group, 1);
+
+ auto* leAudioDevice = group->GetFirstDevice();
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(4);
+
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0);
+
+ InjectInitialIdleNotification(group);
+
+ // Validate GroupStreamStatus
+ EXPECT_CALL(
+ mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::STREAMING));
+
+ // Start the configuration and stream Media content
+ ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type)));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+}
+
+TEST_F(StateMachineTest, testStreamMultipleConversational) {
+ const auto context_type = kContextTypeConversational;
+ const auto leaudio_group_id = 4;
+ const auto num_devices = 2;
+
+ // Prepare multiple fake connected devices in a group
+ auto* group =
+ PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices);
+ ASSERT_EQ(group->Size(), num_devices);
+
+ PrepareConfigureCodecHandler(group);
+ PrepareConfigureQosHandler(group);
+ PrepareEnableHandler(group);
+ PrepareReceiverStartReady(group, 1);
+
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(AtLeast(1));
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(3);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0);
+
+ InjectInitialIdleNotification(group);
+
+ auto* leAudioDevice = group->GetFirstDevice();
+ auto expected_devices_written = 0;
+ while (leAudioDevice) {
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(leAudioDevice->conn_id_,
+ leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(AtLeast(3));
+ expected_devices_written++;
+ leAudioDevice = group->GetNextDevice(leAudioDevice);
+ }
+ ASSERT_EQ(expected_devices_written, num_devices);
+
+ // Validate GroupStreamStatus
+ EXPECT_CALL(
+ mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::STREAMING));
+
+ // Start the configuration and stream Media content
+ ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type)));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+}
+
+TEST_F(StateMachineTest, testStreamMultiple) {
+ const auto context_type = kContextTypeMedia;
+ const auto leaudio_group_id = 4;
+ const auto num_devices = 2;
+
+ // Prepare multiple fake connected devices in a group
+ auto* group =
+ PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices);
+ ASSERT_EQ(group->Size(), num_devices);
+
+ PrepareConfigureCodecHandler(group);
+ PrepareConfigureQosHandler(group);
+ PrepareEnableHandler(group);
+
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(AtLeast(1));
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0);
+
+ InjectInitialIdleNotification(group);
+
+ auto* leAudioDevice = group->GetFirstDevice();
+ auto expected_devices_written = 0;
+ while (leAudioDevice) {
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(leAudioDevice->conn_id_,
+ leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(AtLeast(3));
+ expected_devices_written++;
+ leAudioDevice = group->GetNextDevice(leAudioDevice);
+ }
+ ASSERT_EQ(expected_devices_written, num_devices);
+
+ // Validate GroupStreamStatus
+ EXPECT_CALL(
+ mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::STREAMING));
+
+ // Start the configuration and stream Media content
+ ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type)));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+}
+
+TEST_F(StateMachineTest, testDisableSingle) {
+ const auto context_type = kContextTypeRingtone;
+ const int leaudio_group_id = 4;
+
+ // Prepare fake connected device group
+ auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type);
+
+ /* Since we prepared device with Ringtone context in mind, only one ASE
+ * should have been configured.
+ */
+ PrepareConfigureCodecHandler(group, 1);
+ PrepareConfigureQosHandler(group, 1);
+ PrepareEnableHandler(group, 1);
+ PrepareDisableHandler(group, 1);
+
+ auto* leAudioDevice = group->GetFirstDevice();
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(4);
+
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0);
+
+ InjectInitialIdleNotification(group);
+
+ // Start the configuration and stream Media content
+ LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+
+ // Validate GroupStreamStatus
+ EXPECT_CALL(
+ mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::SUSPENDED));
+
+ // Suspend the stream
+ LeAudioGroupStateMachine::Get()->SuspendStream(group);
+
+ // Check if group has transition to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+}
+
+TEST_F(StateMachineTest, testDisableMultiple) {
+ const auto context_type = kContextTypeMedia;
+ const auto leaudio_group_id = 4;
+ const auto num_devices = 2;
+
+ // Prepare multiple fake connected devices in a group
+ auto* group =
+ PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices);
+ ASSERT_EQ(group->Size(), num_devices);
+
+ PrepareConfigureCodecHandler(group);
+ PrepareConfigureQosHandler(group);
+ PrepareEnableHandler(group);
+ PrepareDisableHandler(group);
+
+ auto* leAudioDevice = group->GetFirstDevice();
+ auto expected_devices_written = 0;
+ while (leAudioDevice) {
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(leAudioDevice->conn_id_,
+ leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(AtLeast(4));
+ expected_devices_written++;
+ leAudioDevice = group->GetNextDevice(leAudioDevice);
+ }
+ ASSERT_EQ(expected_devices_written, num_devices);
+
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(2);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(2);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0);
+
+ InjectInitialIdleNotification(group);
+
+ // Start the configuration and stream Media content
+ LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+
+ // Validate GroupStreamStatus
+ EXPECT_CALL(
+ mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::SUSPENDED));
+
+ // Suspend the stream
+ LeAudioGroupStateMachine::Get()->SuspendStream(group);
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+}
+
+TEST_F(StateMachineTest, testDisableBidirectional) {
+ const auto context_type = kContextTypeConversational;
+ const int leaudio_group_id = 4;
+
+ // Prepare fake connected device group
+ auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type);
+
+ /* Since we prepared device with Conversional context in mind, Sink and Source
+ * ASEs should have been configured.
+ */
+ PrepareConfigureCodecHandler(group, 2);
+ PrepareConfigureQosHandler(group, 2);
+ PrepareEnableHandler(group, 2);
+ PrepareDisableHandler(group, 2);
+ PrepareReceiverStartReady(group, 1);
+ PrepareReceiverStopReady(group, 1);
+
+ auto* leAudioDevice = group->GetFirstDevice();
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(AtLeast(4));
+
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0);
+
+ // Start the configuration and stream Media content
+ LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+
+ // Suspend the stream
+ LeAudioGroupStateMachine::Get()->SuspendStream(group);
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+}
+
+TEST_F(StateMachineTest, testReleaseSingle) {
+ const auto context_type = kContextTypeRingtone;
+ const int leaudio_group_id = 4;
+
+ // Prepare fake connected device group
+ auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type);
+
+ /* Since we prepared device with Ringtone context in mind, only one ASE
+ * should have been configured.
+ */
+ PrepareConfigureCodecHandler(group, 1);
+ PrepareConfigureQosHandler(group, 1);
+ PrepareEnableHandler(group, 1);
+ PrepareDisableHandler(group, 1);
+ PrepareReleaseHandler(group, 1);
+
+ auto* leAudioDevice = group->GetFirstDevice();
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(4);
+
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1);
+
+ InjectInitialIdleNotification(group);
+
+ // Start the configuration and stream Media content
+ LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+
+ // Validate GroupStreamStatus
+ EXPECT_CALL(mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::IDLE));
+
+ // Stop the stream
+ LeAudioGroupStateMachine::Get()->StopStream(group);
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+}
+
+TEST_F(StateMachineTest, testReleaseCachingSingle) {
+ const auto context_type = kContextTypeRingtone;
+ const int leaudio_group_id = 4;
+
+ // Prepare fake connected device group
+ auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type);
+
+ /* Since we prepared device with Ringtone context in mind, only one ASE
+ * should have been configured.
+ */
+ PrepareConfigureCodecHandler(group, 1, true);
+ PrepareConfigureQosHandler(group, 1);
+ PrepareEnableHandler(group, 1);
+ PrepareDisableHandler(group, 1);
+ PrepareReleaseHandler(group, 1);
+
+ auto* leAudioDevice = group->GetFirstDevice();
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(4);
+
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1);
+
+ InjectInitialIdleNotification(group);
+
+ // Validate GroupStreamStatus
+ EXPECT_CALL(mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::IDLE));
+ EXPECT_CALL(
+ mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::STREAMING));
+
+ // Start the configuration and stream Media content
+ LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+
+ // Stop the stream
+ LeAudioGroupStateMachine::Get()->StopStream(group);
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
+}
+
+TEST_F(StateMachineTest, testStreamCachingSingle) {
+ const auto context_type = kContextTypeRingtone;
+ const int leaudio_group_id = 4;
+
+ // Prepare fake connected device group
+ auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type);
+
+ /* Since we prepared device with Ringtone context in mind, only one ASE
+ * should have been configured.
+ */
+ PrepareConfigureCodecHandler(group, 1, true);
+ PrepareConfigureQosHandler(group, 1);
+ PrepareEnableHandler(group, 1);
+ PrepareDisableHandler(group, 1);
+ PrepareReleaseHandler(group, 1);
+
+ auto* leAudioDevice = group->GetFirstDevice();
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(4 + 3);
+
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(2);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(2);
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1);
+
+ InjectInitialIdleNotification(group);
+
+ // Validate GroupStreamStatus
+ EXPECT_CALL(mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::IDLE));
+
+ EXPECT_CALL(mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::STREAMING))
+ .Times(2);
+
+ // Start the configuration and stream Media content
+ LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+
+ // Stop the stream
+ LeAudioGroupStateMachine::Get()->StopStream(group);
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
+
+ // Start the configuration and stream Media content
+ LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+}
+
+TEST_F(StateMachineTest, testReleaseMultiple) {
+ const auto context_type = kContextTypeMedia;
+ const auto leaudio_group_id = 6;
+ const auto num_devices = 2;
+
+ // Prepare multiple fake connected devices in a group
+ auto* group =
+ PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices);
+ ASSERT_EQ(group->Size(), num_devices);
+
+ PrepareConfigureCodecHandler(group);
+ PrepareConfigureQosHandler(group);
+ PrepareEnableHandler(group);
+ PrepareDisableHandler(group);
+ PrepareReleaseHandler(group);
+
+ auto* leAudioDevice = group->GetFirstDevice();
+ auto expected_devices_written = 0;
+ while (leAudioDevice) {
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(leAudioDevice->conn_id_,
+ leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(AtLeast(4));
+ expected_devices_written++;
+ leAudioDevice = group->GetNextDevice(leAudioDevice);
+ }
+ ASSERT_EQ(expected_devices_written, num_devices);
+
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(2);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(2);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1);
+
+ InjectInitialIdleNotification(group);
+
+ // Start the configuration and stream Media content
+ LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+
+ // Validate GroupStreamStatus
+ EXPECT_CALL(mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::IDLE));
+
+ // Stop the stream
+ LeAudioGroupStateMachine::Get()->StopStream(group);
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+}
+
+TEST_F(StateMachineTest, testReleaseBidirectional) {
+ const auto context_type = kContextTypeConversational;
+ const auto leaudio_group_id = 6;
+
+ // Prepare fake connected device group
+ auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type);
+
+ /* Since we prepared device with Conversional context in mind, Sink and Source
+ * ASEs should have been configured.
+ */
+ PrepareConfigureCodecHandler(group, 2);
+ PrepareConfigureQosHandler(group, 2);
+ PrepareEnableHandler(group, 2);
+ PrepareDisableHandler(group, 2);
+ PrepareReceiverStartReady(group, 1);
+ PrepareReleaseHandler(group, 2);
+
+ auto* leAudioDevice = group->GetFirstDevice();
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(AtLeast(4));
+
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1);
+
+ InjectInitialIdleNotification(group);
+
+ // Start the configuration and stream Media content
+ LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type));
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(),
+ types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+
+ // Stop the stream
+ LeAudioGroupStateMachine::Get()->StopStream(group);
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+}
+
+TEST_F(StateMachineTest, testDisableAndReleaseBidirectional) {
+ const auto context_type = kContextTypeConversational;
+ const int leaudio_group_id = 4;
+
+ // Prepare fake connected device group
+ auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type);
+
+ /* Since we prepared device with Conversional context in mind, Sink and Source
+ * ASEs should have been configured.
+ */
+ PrepareConfigureCodecHandler(group, 2);
+ PrepareConfigureQosHandler(group, 2);
+ PrepareEnableHandler(group, 2);
+ PrepareDisableHandler(group, 2);
+ PrepareReceiverStartReady(group, 1);
+ PrepareReceiverStopReady(group, 1);
+ PrepareReleaseHandler(group, 2);
+
+ auto* leAudioDevice = group->GetFirstDevice();
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(AtLeast(4));
+
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1);
+
+ // Start the configuration and stream Media content
+ LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type));
+
+ // Suspend the stream
+ LeAudioGroupStateMachine::Get()->SuspendStream(group);
+
+ // Stop the stream
+ LeAudioGroupStateMachine::Get()->StopStream(group);
+
+ // Check if group has transitioned to a proper state
+ ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+}
+
+TEST_F(StateMachineTest, testAseIdAssignmentIdle) {
+ const auto context_type = kContextTypeConversational;
+ const auto leaudio_group_id = 6;
+ const auto num_devices = 1;
+
+ // Prepare multiple fake connected devices in a group
+ auto* group =
+ PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices);
+ ASSERT_EQ(group->Size(), num_devices);
+
+ // Should not trigger any action on our side
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(_, _, _, _, _, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0);
+
+ for (auto* device = group->GetFirstDevice(); device != nullptr;
+ device = group->GetNextDevice(device)) {
+ for (auto& ase : device->ases_) {
+ ASSERT_EQ(ase.id, le_audio::types::ase::kAseIdInvalid);
+ InjectAseStateNotification(&ase, device, group, ascs::kAseStateIdle,
+ nullptr);
+ ASSERT_EQ(ase.id, ase_id_last_assigned);
+ }
+ }
+}
+
+TEST_F(StateMachineTest, testAseIdAssignmentCodecConfigured) {
+ const auto context_type = kContextTypeConversational;
+ const auto leaudio_group_id = 6;
+ const auto num_devices = 1;
+
+ // Prepare multiple fake connected devices in a group
+ auto* group =
+ PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices);
+ ASSERT_EQ(group->Size(), num_devices);
+
+ // Should not trigger any action on our side
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(_, _, _, _, _, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0);
+ EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0);
+
+ for (auto* device = group->GetFirstDevice(); device != nullptr;
+ device = group->GetNextDevice(device)) {
+ for (auto& ase : device->ases_) {
+ client_parser::ascs::ase_codec_configured_state_params
+ codec_configured_state_params;
+
+ ASSERT_EQ(ase.id, le_audio::types::ase::kAseIdInvalid);
+ InjectAseStateNotification(&ase, device, group,
+ ascs::kAseStateCodecConfigured,
+ &codec_configured_state_params);
+ ASSERT_EQ(ase.id, ase_id_last_assigned);
+ }
+ }
+}
+
+TEST_F(StateMachineTest, testAseAutonomousRelease) {
+ const auto context_type = kContextTypeConversational;
+ const int leaudio_group_id = 4;
+
+ // Prepare fake connected device group
+ auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type);
+
+ /* Since we prepared device with Conversional context in mind, Sink and Source
+ * ASEs should have been configured.
+ */
+ PrepareConfigureCodecHandler(group, 2);
+ PrepareConfigureQosHandler(group, 2);
+ PrepareEnableHandler(group, 2);
+ PrepareDisableHandler(group, 2);
+ PrepareReceiverStartReady(group, 1);
+ PrepareReceiverStopReady(group, 1);
+ PrepareReleaseHandler(group, 2);
+
+ InjectInitialIdleNotification(group);
+
+ // Validate initial GroupStreamStatus
+ EXPECT_CALL(
+ mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::STREAMING));
+
+ // Start the configuration and stream Media content
+ ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type)));
+
+ // Validate new GroupStreamStatus
+ EXPECT_CALL(mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::IDLE));
+
+ for (auto* device = group->GetFirstDevice(); device != nullptr;
+ device = group->GetNextDevice(device)) {
+ for (auto& ase : device->ases_) {
+ client_parser::ascs::ase_codec_configured_state_params
+ codec_configured_state_params;
+
+ ASSERT_EQ(ase.state, types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+
+ // Each one does the autonomous release
+ InjectAseStateNotification(&ase, device, group, ascs::kAseStateReleasing,
+ &codec_configured_state_params);
+ InjectAseStateNotification(&ase, device, group, ascs::kAseStateIdle,
+ &codec_configured_state_params);
+ }
+ }
+
+ // Verify we've handled the release and updated all states
+ for (auto* device = group->GetFirstDevice(); device != nullptr;
+ device = group->GetNextDevice(device)) {
+ for (auto& ase : device->ases_) {
+ ASSERT_EQ(ase.state, types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+ }
+ }
+}
+
+TEST_F(StateMachineTest, testStateTransitionTimeout) {
+ const auto context_type = kContextTypeRingtone;
+ const int leaudio_group_id = 4;
+
+ // Prepare fake connected device group
+ auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type);
+
+ /* Since we prepared device with Ringtone context in mind, only one ASE
+ * should have been configured.
+ */
+ PrepareConfigureCodecHandler(group, 1);
+ PrepareConfigureQosHandler(group, 1);
+
+ auto* leAudioDevice = group->GetFirstDevice();
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _,
+ GATT_WRITE_NO_RSP, _, _))
+ .Times(3);
+
+ // Start the configuration and stream Media content
+ ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream(
+ group, static_cast<types::LeAudioContextType>(context_type)));
+
+ // Check if timeout is fired
+ EXPECT_CALL(mock_callbacks_, OnStateTransitionTimeout(leaudio_group_id));
+
+ // simulate timeout seconds passed, alarm executing
+ fake_osi_alarm_set_on_mloop_.cb(fake_osi_alarm_set_on_mloop_.data);
+ ASSERT_EQ(1, mock_function_count_map["alarm_set_on_mloop"]);
+}
+} // namespace internal
+} // namespace le_audio
diff --git a/bta/test/common/btif_storage_mock.cc b/bta/test/common/btif_storage_mock.cc
new file mode 100644
index 000000000..1d389c680
--- /dev/null
+++ b/bta/test/common/btif_storage_mock.cc
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+#include "btif_storage_mock.h"
+
+#include <base/logging.h>
+
+static bluetooth::storage::MockBtifStorageInterface* btif_storage_interface =
+ nullptr;
+
+void bluetooth::storage::SetMockBtifStorageInterface(
+ MockBtifStorageInterface* mock_btif_storage_interface) {
+ btif_storage_interface = mock_btif_storage_interface;
+}
+
+void btif_storage_set_leaudio_autoconnect(RawAddress const& addr,
+ bool autoconnect) {
+ LOG_ASSERT(btif_storage_interface) << "Mock storage module not set!";
+ btif_storage_interface->AddLeaudioAutoconnect(addr, autoconnect);
+}
+
+void btif_storage_remove_leaudio(RawAddress const& addr) {
+ LOG_ASSERT(btif_storage_interface) << "Mock storage module not set!";
+ btif_storage_interface->RemoveLeaudio(addr);
+} \ No newline at end of file
diff --git a/bta/test/common/btif_storage_mock.h b/bta/test/common/btif_storage_mock.h
new file mode 100644
index 000000000..1087bc9c5
--- /dev/null
+++ b/bta/test/common/btif_storage_mock.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+#pragma once
+
+#include <gmock/gmock.h>
+
+#include "types/raw_address.h"
+
+namespace bluetooth {
+namespace storage {
+
+class BtifStorageInterface {
+ public:
+ virtual void AddLeaudioAutoconnect(RawAddress const& addr,
+ bool autoconnect) = 0;
+ virtual void RemoveLeaudio(RawAddress const& addr) = 0;
+ virtual ~BtifStorageInterface() = default;
+};
+
+class MockBtifStorageInterface : public BtifStorageInterface {
+ public:
+ MOCK_METHOD((void), AddLeaudioAutoconnect,
+ (RawAddress const& addr, bool autoconnect), (override));
+ MOCK_METHOD((void), RemoveLeaudio, (RawAddress const& addr), (override));
+};
+
+/**
+ * Set the {@link MockBifStorageInterface} for testing
+ *
+ * @param mock_btif_storage_interface pointer to mock btm security
+ * internal interface, could be null
+ */
+void SetMockBtifStorageInterface(
+ MockBtifStorageInterface* mock_btif_storage_interface);
+
+} // namespace storage
+} // namespace bluetooth
diff --git a/btif/Android.bp b/btif/Android.bp
index c9806ce74..2e70a0cb9 100644
--- a/btif/Android.bp
+++ b/btif/Android.bp
@@ -178,7 +178,10 @@ cc_library_static {
srcs: ["src/btif_avrcp_audio_track.cc"],
},
host: {
- srcs: ["src/btif_avrcp_audio_track_linux.cc"],
+ srcs: [
+ "src/btif_avrcp_audio_track_linux.cc",
+ "src/btif_leaudio_hal_version_host.cc",
+ ],
},
},
whole_static_libs: [
@@ -228,6 +231,7 @@ cc_test {
"libbt-utils",
"libFraunhoferAAC",
"libg722codec",
+ "liblc3codec",
"libbtdevice",
"libbt-hci",
"libudrv-uipc",
@@ -405,6 +409,7 @@ cc_test {
":TestMockBtaHf",
":TestMockBtaHh",
":TestMockBtaJv",
+ ":TestMockBtaLeAudio",
":TestMockBtaPan",
":TestMockBtaSdp",
":TestMockBtaSys",
diff --git a/btif/include/btif_storage.h b/btif/include/btif_storage.h
index c5aee96f9..a86ad6090 100644
--- a/btif/include/btif_storage.h
+++ b/btif/include/btif_storage.h
@@ -264,6 +264,16 @@ bool btif_storage_get_hearing_aid_prop(
const RawAddress& address, uint8_t* capabilities, uint64_t* hi_sync_id,
uint16_t* render_delay, uint16_t* preparation_delay, uint16_t* codecs);
+/** Store Le Audio device autoconnect flag */
+void btif_storage_set_leaudio_autoconnect(const RawAddress& addr,
+ bool autoconnect);
+
+/** Remove Le Audio device from the storage */
+void btif_storage_remove_leaudio(const RawAddress& address);
+
+/** Load bonded Le Audio devices */
+void btif_storage_load_bonded_leaudio(void);
+
/*******************************************************************************
*
* Function btif_storage_is_retricted_device
diff --git a/btif/src/bluetooth.cc b/btif/src/bluetooth.cc
index 8df55805d..1fec5b54a 100644
--- a/btif/src/bluetooth.cc
+++ b/btif/src/bluetooth.cc
@@ -51,6 +51,7 @@
#include "bta/include/bta_csis_api.h"
#include "bta/include/bta_hearing_aid_api.h"
#include "bta/include/bta_hf_client_api.h"
+#include "bta/include/bta_le_audio_api.h"
#include "btif/avrcp/avrcp_service.h"
#include "btif/include/stack_manager.h"
#include "btif_a2dp.h"
@@ -403,6 +404,7 @@ static void dump(int fd, const char** arguments) {
alarm_debug_dump(fd);
bluetooth::csis::CsisClient::DebugDump(fd);
HearingAid::DebugDump(fd);
+ LeAudioClient::DebugDump(fd);
connection_manager::dump(fd);
bluetooth::bqr::DebugDump(fd);
if (bluetooth::shim::is_any_gd_enabled()) {
diff --git a/btif/src/btif_dm.cc b/btif/src/btif_dm.cc
index 51600ed09..f938987c5 100644
--- a/btif/src/btif_dm.cc
+++ b/btif/src/btif_dm.cc
@@ -35,6 +35,7 @@
#include <hardware/bluetooth.h>
#include <hardware/bt_csis.h>
#include <hardware/bt_hearing_aid.h>
+#include <hardware/bt_le_audio.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
@@ -49,6 +50,7 @@
#include "bta_csis_api.h"
#include "bta_dm_int.h"
#include "bta_gatt_api.h"
+#include "bta_le_audio_api.h"
#include "btif/include/stack_manager.h"
#include "btif_api.h"
#include "btif_av.h"
@@ -86,6 +88,7 @@ using bluetooth::Uuid;
const Uuid UUID_HEARING_AID = Uuid::FromString("FDF0");
const Uuid UUID_VC = Uuid::FromString("1844");
const Uuid UUID_CSIS = Uuid::FromString("1846");
+const Uuid UUID_LE_AUDIO = Uuid::FromString("184E");
#define COD_MASK 0x07FF
@@ -244,6 +247,8 @@ extern bt_status_t btif_hd_execute_service(bool b_enable);
extern bluetooth::hearing_aid::HearingAidInterface*
btif_hearing_aid_get_interface();
extern bluetooth::csis::CsisClientInterface* btif_csis_client_get_interface();
+extern bluetooth::le_audio::LeAudioClientInterface*
+btif_le_audio_get_interface();
/******************************************************************************
* Functions
@@ -1304,7 +1309,7 @@ static void btif_dm_search_devices_evt(tBTA_DM_SEARCH_EVT event,
/* Returns true if |uuid| should be passed as device property */
static bool btif_is_interesting_le_service(bluetooth::Uuid uuid) {
return (uuid.As16Bit() == UUID_SERVCLASS_LE_HID || uuid == UUID_HEARING_AID ||
- uuid == UUID_VC || uuid == UUID_CSIS);
+ uuid == UUID_VC || uuid == UUID_CSIS || uuid == UUID_LE_AUDIO);
}
/*******************************************************************************
@@ -1592,6 +1597,9 @@ static void btif_dm_upstreams_evt(uint16_t event, char* p_param) {
if (bluetooth::csis::CsisClient::IsCsisClientRunning())
btif_csis_client_get_interface()->RemoveDevice(bd_addr);
+ if (LeAudioClient::IsLeAudioClientRunning())
+ btif_le_audio_get_interface()->RemoveDevice(bd_addr);
+
btif_storage_remove_bonded_device(&bd_addr);
bond_state_changed(BT_STATUS_SUCCESS, bd_addr, BT_BOND_STATE_NONE);
break;
diff --git a/btif/src/btif_le_audio.cc b/btif/src/btif_le_audio.cc
index dea691b3e..73a8e0fb4 100644
--- a/btif/src/btif_le_audio.cc
+++ b/btif/src/btif_le_audio.cc
@@ -20,10 +20,11 @@
#include <vector>
+#include "audio_hal_interface/hal_version_manager.h"
+#include "bta_le_audio_api.h"
#include "btif_common.h"
#include "btif_storage.h"
-#include "hardware/bt_le_audio.h"
-#include "types/raw_address.h"
+#include "stack/include/btu.h"
using base::Bind;
using base::Unretained;
@@ -70,21 +71,69 @@ class LeAudioClientInterfaceImpl : public LeAudioClientInterface,
void Initialize(LeAudioClientCallbacks* callbacks) override {
this->callbacks = callbacks;
+ do_in_main_thread(
+ FROM_HERE,
+ Bind(&LeAudioClient::Initialize, this,
+ jni_thread_wrapper(FROM_HERE,
+ Bind(&btif_storage_load_bonded_leaudio)),
+ base::Bind([]() -> bool {
+ return bluetooth::audio::HalVersionManager::GetHalVersion() ==
+ bluetooth::audio::BluetoothAudioHalVersion::VERSION_2_1;
+ })));
}
- void Cleanup(void) override {}
+ void Cleanup(void) override {
+ DVLOG(2) << __func__;
+ do_in_main_thread(FROM_HERE, Bind(&LeAudioClient::Cleanup));
+ }
+
+ void RemoveDevice(const RawAddress& address) override {
+ DVLOG(2) << __func__ << " address: " << address;
+ do_in_main_thread(FROM_HERE,
+ Bind(&LeAudioClient::RemoveDevice,
+ Unretained(LeAudioClient::Get()), address));
- void RemoveDevice(const RawAddress& address) override {}
+ do_in_jni_thread(FROM_HERE, Bind(&btif_storage_remove_leaudio, address));
+ }
- void Connect(const RawAddress& address) override {}
+ void Connect(const RawAddress& address) override {
+ DVLOG(2) << __func__ << " address: " << address;
+ do_in_main_thread(FROM_HERE,
+ Bind(&LeAudioClient::Connect,
+ Unretained(LeAudioClient::Get()), address));
+ }
- void GroupAddNode(const int group_id, const RawAddress& addr) override {}
+ void Disconnect(const RawAddress& address) override {
+ DVLOG(2) << __func__ << " address: " << address;
+ do_in_main_thread(FROM_HERE,
+ Bind(&LeAudioClient::Disconnect,
+ Unretained(LeAudioClient::Get()), address));
+ do_in_jni_thread(
+ FROM_HERE, Bind(&btif_storage_set_leaudio_autoconnect, address, false));
+ }
- void Disconnect(const RawAddress& address) override {}
+ void GroupAddNode(const int group_id, const RawAddress& address) override {
+ DVLOG(2) << __func__ << " group_id: " << group_id
+ << " address: " << address;
+ do_in_main_thread(
+ FROM_HERE, Bind(&LeAudioClient::GroupAddNode,
+ Unretained(LeAudioClient::Get()), group_id, address));
+ }
- void GroupRemoveNode(const int group_id, const RawAddress& addr) override {}
+ void GroupRemoveNode(const int group_id, const RawAddress& address) override {
+ DVLOG(2) << __func__ << " group_id: " << group_id
+ << " address: " << address;
+ do_in_main_thread(
+ FROM_HERE, Bind(&LeAudioClient::GroupRemoveNode,
+ Unretained(LeAudioClient::Get()), group_id, address));
+ }
- void GroupSetActive(const int group_id) override {}
+ void GroupSetActive(const int group_id) override {
+ DVLOG(2) << __func__ << " group_id: " << group_id;
+ do_in_main_thread(FROM_HERE,
+ Bind(&LeAudioClient::GroupSetActive,
+ Unretained(LeAudioClient::Get()), group_id));
+ }
private:
LeAudioClientCallbacks* callbacks;
diff --git a/btif/src/btif_leaudio_hal_version_host.cc b/btif/src/btif_leaudio_hal_version_host.cc
new file mode 100644
index 000000000..a093f80ad
--- /dev/null
+++ b/btif/src/btif_leaudio_hal_version_host.cc
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021 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.
+ */
+
+#include <memory>
+
+#include "embdrv/lc3/Api/Lc3Encoder.hpp"
+
+namespace bluetooth {
+namespace audio {
+class HalVersionManager {
+ static std::unique_ptr<HalVersionManager> instance_ptr;
+};
+} // namespace audio
+} // namespace bluetooth
+
+std::unique_ptr<bluetooth::audio::HalVersionManager>
+ bluetooth::audio::HalVersionManager::instance_ptr;
diff --git a/btif/src/btif_storage.cc b/btif/src/btif_storage.cc
index 572005c8a..38640e235 100644
--- a/btif/src/btif_storage.cc
+++ b/btif/src/btif_storage.cc
@@ -44,6 +44,7 @@
#include "bta_hd_api.h"
#include "bta_hearing_aid_api.h"
#include "bta_hh_api.h"
+#include "bta_le_audio_api.h"
#include "btif_api.h"
#include "btif_config.h"
#include "btif_hd.h"
@@ -94,6 +95,7 @@ using bluetooth::groups::DeviceGroups;
#define BTIF_STORAGE_DEVICE_GROUP_BIN "DeviceGroupBin"
#define BTIF_STORAGE_CSIS_AUTOCONNECT "CsisAutoconnect"
#define BTIF_STORAGE_CSIS_SET_INFO_BIN "CsisSetInfoBin"
+#define BTIF_STORAGE_LEAUDIO_AUTOCONNECT "LeAudioAutoconnect"
/* This is a local property to add a device found */
#define BT_PROPERTY_REMOTE_DEVICE_TIMESTAMP 0xFF
@@ -1735,6 +1737,64 @@ uint8_t btif_storage_get_sr_supp_feat(const RawAddress& bd_addr) {
return value;
}
+/** Set autoconnect information for LeAudio device */
+void btif_storage_set_leaudio_autoconnect(const RawAddress& addr,
+ bool autoconnect) {
+ do_in_jni_thread(FROM_HERE, Bind(
+ [](const RawAddress& addr, bool autoconnect) {
+ std::string bdstr = addr.ToString();
+ VLOG(2)
+ << "saving le audio device: " << bdstr;
+ btif_config_set_int(
+ bdstr, BTIF_STORAGE_LEAUDIO_AUTOCONNECT,
+ autoconnect);
+ btif_config_save();
+ },
+ addr, autoconnect));
+}
+
+/** Loads information about bonded Le Audio devices */
+void btif_storage_load_bonded_leaudio() {
+ for (const auto& bd_addr : btif_config_get_paired_devices()) {
+ auto name = bd_addr.ToString();
+
+ int size = STORAGE_UUID_STRING_SIZE * BT_MAX_NUM_UUIDS;
+ char uuid_str[size];
+ bool isLeAudioDevice = false;
+ if (btif_config_get_str(name, BTIF_STORAGE_PATH_REMOTE_SERVICE, uuid_str,
+ &size)) {
+ Uuid p_uuid[BT_MAX_NUM_UUIDS];
+ size_t num_uuids =
+ btif_split_uuids_string(uuid_str, p_uuid, BT_MAX_NUM_UUIDS);
+ for (size_t i = 0; i < num_uuids; i++) {
+ if (p_uuid[i] == Uuid::FromString("184E")) {
+ isLeAudioDevice = true;
+ break;
+ }
+ }
+ }
+ if (!isLeAudioDevice) {
+ continue;
+ }
+
+ BTIF_TRACE_DEBUG("Remote device:%s", name.c_str());
+
+ int value;
+ bool autoconnect = false;
+ if (btif_config_get_int(name, BTIF_STORAGE_LEAUDIO_AUTOCONNECT, &value))
+ autoconnect = !!value;
+
+ do_in_main_thread(
+ FROM_HERE, Bind(&LeAudioClient::AddFromStorage, bd_addr, autoconnect));
+ }
+}
+
+/** Remove the Le Audio device from storage */
+void btif_storage_remove_leaudio(const RawAddress& address) {
+ std::string addrstr = address.ToString();
+ btif_config_set_int(addrstr, BTIF_STORAGE_LEAUDIO_AUTOCONNECT, false);
+}
+
/** Adds the bonded Le Audio device grouping info into the NVRAM */
void btif_storage_add_groups(const RawAddress& addr) {
std::vector<uint8_t> group_info;
diff --git a/embdrv/lc3/Android.bp b/embdrv/lc3/Android.bp
index 2ba76485c..ce1302f85 100644
--- a/embdrv/lc3/Android.bp
+++ b/embdrv/lc3/Android.bp
@@ -33,13 +33,17 @@ cc_library_static {
"-Wno-self-assign",
"-Wno-implicit-fallthrough",
],
- sanitize: {
- misc_undefined:[
- "unsigned-integer-overflow",
- "signed-integer-overflow",
- "bounds",
- ],
- cfi: true,
+ target: {
+ android: {
+ sanitize: {
+ misc_undefined:[
+ "unsigned-integer-overflow",
+ "signed-integer-overflow",
+ "bounds",
+ ],
+ cfi: true,
+ },
+ },
},
shared_libs: [
"liblog",
diff --git a/gd/rust/topshim/facade/Android.bp b/gd/rust/topshim/facade/Android.bp
index feb649fb1..95f514b1b 100644
--- a/gd/rust/topshim/facade/Android.bp
+++ b/gd/rust/topshim/facade/Android.bp
@@ -43,6 +43,7 @@ rust_binary_host {
"libbt-sbc-encoder",
"libFraunhoferAAC",
"libg722codec",
+ "liblc3codec",
"libudrv-uipc",
"libbluetooth_gd", // Gabeldorsche
"libbluetooth_rust_interop",
diff --git a/include/hardware/bluetooth.h b/include/hardware/bluetooth.h
index 0810af92f..28069dbee 100644
--- a/include/hardware/bluetooth.h
+++ b/include/hardware/bluetooth.h
@@ -54,6 +54,7 @@
#define BT_ACTIVITY_ATTRIBUTION_ID "activity_attribution"
#define BT_PROFILE_VC_ID "volume_control"
#define BT_PROFILE_CSIS_CLIENT_ID "csis_client"
+#define BT_PROFILE_LE_AUDIO_ID "le_audio"
/** Bluetooth Device Name */
typedef struct { uint8_t name[249]; } __attribute__((packed)) bt_bdname_t;
diff --git a/main/Android.bp b/main/Android.bp
index 4b5b8bfda..8b8762c34 100644
--- a/main/Android.bp
+++ b/main/Android.bp
@@ -103,7 +103,7 @@ cc_library_shared {
"libbt-sbc-encoder",
"libFraunhoferAAC",
"libg722codec",
- "liblc3codec",
+ "liblc3codec",
"libudrv-uipc",
"libprotobuf-cpp-lite",
"libbluetooth_gd", // Gabeldorsche
diff --git a/stack/test/fuzzers/Android.bp b/stack/test/fuzzers/Android.bp
index c26720982..5d51ad663 100644
--- a/stack/test/fuzzers/Android.bp
+++ b/stack/test/fuzzers/Android.bp
@@ -34,6 +34,7 @@ cc_defaults {
"libbt-hci",
"libbtdevice",
"libg722codec",
+ "liblc3codec",
"libosi",
"libudrv-uipc",
"libbt-protos-lite",
diff --git a/test/Android.bp b/test/Android.bp
index a7806d638..3f5fea754 100644
--- a/test/Android.bp
+++ b/test/Android.bp
@@ -97,6 +97,13 @@ filegroup {
}
filegroup {
+ name: "TestMockBtaLeAudio",
+ srcs: [
+ "mock/mock_bta_leaudio*.cc",
+ ],
+}
+
+filegroup {
name: "TestMockBtaPan",
srcs: [
"mock/mock_bta_pan_*.cc",
@@ -139,6 +146,7 @@ filegroup {
":TestMockBtaHf",
":TestMockBtaHh",
":TestMockBtaJv",
+ ":TestMockBtaLeAudio",
":TestMockBtaPan",
":TestMockBtaSdp",
":TestMockBtaSys",
diff --git a/test/mock/mock_bta_leaudio.cc b/test/mock/mock_bta_leaudio.cc
new file mode 100644
index 000000000..b44269709
--- /dev/null
+++ b/test/mock/mock_bta_leaudio.cc
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2021 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.
+ */
+
+/*
+ * Generated mock file from original source file
+ * Functions generated:7
+ */
+
+#include <map>
+#include <memory>
+#include <string>
+
+extern std::map<std::string, int> mock_function_count_map;
+
+#include <base/bind.h>
+#include <base/bind_helpers.h>
+#include <hardware/bt_le_audio.h>
+
+#include "bta/include/bta_le_audio_api.h"
+#include "types/raw_address.h"
+
+#ifndef UNUSED_ATTR
+#define UNUSED_ATTR
+#endif
+
+/* Empty class to satisfy compiler */
+namespace bluetooth {
+namespace audio {
+class HalVersionManager {
+ static std::unique_ptr<HalVersionManager> instance_ptr;
+};
+} // namespace audio
+} // namespace bluetooth
+
+void LeAudioClient::AddFromStorage(const RawAddress& address,
+ bool auto_connect) {
+ mock_function_count_map[__func__]++;
+}
+void LeAudioClient::Cleanup() { mock_function_count_map[__func__]++; }
+
+LeAudioClient* LeAudioClient::Get(void) {
+ mock_function_count_map[__func__]++;
+ return nullptr;
+}
+bool LeAudioClient::IsLeAudioClientRunning(void) {
+ mock_function_count_map[__func__]++;
+ return false;
+}
+void LeAudioClient::Initialize(
+ bluetooth::le_audio::LeAudioClientCallbacks* callbacks_,
+ base::Closure initCb, base::Callback<bool()> hal_2_1_verifier) {
+ mock_function_count_map[__func__]++;
+}
+void LeAudioClient::DebugDump(int fd) { mock_function_count_map[__func__]++; } \ No newline at end of file