| /* |
| * 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; |
| |
| std::map<std::string, int> mock_function_count_map; |
| |
| // 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, uint16_t len, |
| uint8_t* value, void* cb_data) { |
| cb(conn_id, GATT_SUCCESS, handle, len, |
| value, cb_data); |
| }, |
| cb, conn_id, handle, value.size(), |
| value.data(), 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; |
| |
| /* Assume CIG is created */ |
| group->cig_created_ = true; |
| |
| 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([this](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; |
| } |
| } |
| |
| if (group->IsEmpty()) { |
| group->cig_created_ = false; |
| InjectCigRemoved(group->group_id_); |
| } |
| }); |
| |
| 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; |
| |
| struct playback_track_metadata tracks_[2] = { |
| {AUDIO_USAGE_UNKNOWN, AUDIO_CONTENT_TYPE_UNKNOWN, 0}, |
| {AUDIO_USAGE_UNKNOWN, AUDIO_CONTENT_TYPE_UNKNOWN, 0}}; |
| |
| source_metadata_t source_metadata = {.track_count = 1, |
| .tracks = &tracks_[0]}; |
| |
| tracks_[0].usage = usage; |
| tracks_[0].content_type = content_type; |
| |
| auto do_metadata_update_future = do_metadata_update_promise.get_future(); |
| audio_sink_receiver_->OnAudioMetadataUpdate( |
| std::move(do_metadata_update_promise), source_metadata); |
| 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); |
| } |
| |
| void InjectCigRemoved(uint8_t cig_id) { |
| bluetooth::hci::iso_manager::cig_remove_cmpl_evt evt; |
| evt.status = 0; |
| evt.cig_id = cig_id; |
| |
| ASSERT_NE(cig_callbacks_, nullptr); |
| cig_callbacks_->OnCisEvent( |
| bluetooth::hci::iso_manager::kIsoEventCigOnRemoveCmpl, &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, false, _)) |
| .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, false, _)) |
| .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, false, _)) |
| .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, false, _)) |
| .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, RemoveWhileStreaming) { |
| 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); |
| |
| EXPECT_CALL(mock_state_machine_, StartStream(_, _)).Times(1); |
| |
| StartStreaming(AUDIO_USAGE_MEDIA, AUDIO_CONTENT_TYPE_MUSIC, group_id); |
| |
| SyncOnMainLoop(); |
| Mock::VerifyAndClearExpectations(&mock_client_callbacks_); |
| Mock::VerifyAndClearExpectations(&mock_audio_source_); |
| Mock::VerifyAndClearExpectations(&mock_state_machine_); |
| SyncOnMainLoop(); |
| |
| // Verify Data transfer on one audio source cis |
| TestAudioDataTransfer(group_id, cis_count_out, cis_count_in, 1920); |
| |
| EXPECT_CALL(mock_groups_module_, RemoveDevice(test_address0, group_id)) |
| .Times(1); |
| |
| LeAudioDeviceGroup* group = nullptr; |
| EXPECT_CALL(mock_state_machine_, ProcessHciNotifAclDisconnected(_, _)) |
| .WillOnce(DoAll(SaveArg<0>(&group))); |
| EXPECT_CALL( |
| mock_client_callbacks_, |
| OnGroupNodeStatus(test_address0, group_id, GroupNodeStatus::REMOVED)); |
| |
| EXPECT_CALL(mock_client_callbacks_, |
| OnConnectionState(ConnectionState::DISCONNECTED, test_address0)) |
| .Times(1); |
| |
| LeAudioClient::Get()->RemoveDevice(test_address0); |
| |
| SyncOnMainLoop(); |
| Mock::VerifyAndClearExpectations(&mock_groups_module_); |
| Mock::VerifyAndClearExpectations(&mock_state_machine_); |
| Mock::VerifyAndClearExpectations(&mock_client_callbacks_); |
| |
| ASSERT_NE(group, nullptr); |
| } |
| |
| 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 |