| /* |
| * Copyright 2021 HIMSA II K/S - www.himsa.com. Represented by EHIMA - |
| * www.ehima.com |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include "state_machine.h" |
| |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include <functional> |
| |
| #include "bta_gatt_api_mock.h" |
| #include "bta_gatt_queue_mock.h" |
| #include "btm_api_mock.h" |
| #include "client_parser.h" |
| #include "fake_osi.h" |
| #include "mock_controller.h" |
| #include "mock_iso_manager.h" |
| #include "types/bt_transport.h" |
| |
| using ::testing::_; |
| using ::testing::AnyNumber; |
| using ::testing::AtLeast; |
| using ::testing::DoAll; |
| using ::testing::Invoke; |
| using ::testing::Return; |
| using ::testing::SaveArg; |
| using ::testing::Test; |
| |
| std::map<std::string, int> mock_function_count_map; |
| |
| extern struct fake_osi_alarm_set_on_mloop fake_osi_alarm_set_on_mloop_; |
| |
| namespace le_audio { |
| namespace internal { |
| |
| // Just some arbitrary initial handles - it has no real meaning |
| #define ATTR_HANDLE_ASCS_POOL_START (0x0000 | 32) |
| #define ATTR_HANDLE_PACS_POOL_START (0xFF00 | 64) |
| |
| constexpr uint16_t kContextTypeUnspecified = 0x0001; |
| constexpr uint16_t kContextTypeConversational = 0x0002; |
| constexpr uint16_t kContextTypeMedia = 0x0004; |
| // constexpr uint16_t kContextTypeInstructional = 0x0008; |
| // constexpr uint16_t kContextTypeAttentionSeeking = 0x0010; |
| // constexpr uint16_t kContextTypeImmediateAllert = 0x0020; |
| // constexpr uint16_t kContextTypeManMachine = 0x0040; |
| // constexpr uint16_t kContextTypeEmergencyAlert = 0x0080; |
| constexpr uint16_t kContextTypeRingtone = 0x0100; |
| // constexpr uint16_t kContextTypeTV = 0x0200; |
| // constexpr uint16_t kContextTypeRFULive = 0x0400; |
| |
| namespace codec_specific { |
| |
| constexpr uint8_t kLc3CodingFormat = 0x06; |
| |
| // Reference Codec Capabilities values to test against |
| constexpr uint8_t kCapTypeSupportedSamplingFrequencies = 0x01; |
| constexpr uint8_t kCapTypeSupportedFrameDurations = 0x02; |
| constexpr uint8_t kCapTypeAudioChannelCount = 0x03; |
| constexpr uint8_t kCapTypeSupportedOctetsPerCodecFrame = 0x04; |
| // constexpr uint8_t kCapTypeSupportedLc3CodecFramesPerSdu = 0x05; |
| |
| // constexpr uint8_t kCapSamplingFrequency8000Hz = 0x0001; |
| // constexpr uint8_t kCapSamplingFrequency11025Hz = 0x0002; |
| constexpr uint8_t kCapSamplingFrequency16000Hz = 0x0004; |
| // constexpr uint8_t kCapSamplingFrequency22050Hz = 0x0008; |
| // constexpr uint8_t kCapSamplingFrequency24000Hz = 0x0010; |
| // constexpr uint8_t kCapSamplingFrequency32000Hz = 0x0020; |
| // constexpr uint8_t kCapSamplingFrequency44100Hz = 0x0040; |
| // constexpr uint8_t kCapSamplingFrequency48000Hz = 0x0080; |
| // constexpr uint8_t kCapSamplingFrequency88200Hz = 0x0100; |
| // constexpr uint8_t kCapSamplingFrequency96000Hz = 0x0200; |
| // constexpr uint8_t kCapSamplingFrequency176400Hz = 0x0400; |
| // constexpr uint8_t kCapSamplingFrequency192000Hz = 0x0800; |
| // constexpr uint8_t kCapSamplingFrequency384000Hz = 0x1000; |
| |
| constexpr uint8_t kCapFrameDuration7p5ms = 0x01; |
| constexpr uint8_t kCapFrameDuration10ms = 0x02; |
| // constexpr uint8_t kCapFrameDuration7p5msPreferred = 0x10; |
| constexpr uint8_t kCapFrameDuration10msPreferred = 0x20; |
| } // namespace codec_specific |
| |
| namespace ascs { |
| constexpr uint8_t kAseStateIdle = 0x00; |
| constexpr uint8_t kAseStateCodecConfigured = 0x01; |
| constexpr uint8_t kAseStateQoSConfigured = 0x02; |
| constexpr uint8_t kAseStateEnabling = 0x03; |
| constexpr uint8_t kAseStateStreaming = 0x04; |
| constexpr uint8_t kAseStateDisabling = 0x05; |
| constexpr uint8_t kAseStateReleasing = 0x06; |
| |
| // constexpr uint8_t kAseParamDirectionServerIsAudioSink = 0x01; |
| // constexpr uint8_t kAseParamDirectionServerIsAudioSource = 0x02; |
| |
| constexpr uint8_t kAseParamFramingUnframedSupported = 0x00; |
| // constexpr uint8_t kAseParamFramingUnframedNotSupported = 0x01; |
| |
| // constexpr uint8_t kAseParamPreferredPhy1M = 0x01; |
| // constexpr uint8_t kAseParamPreferredPhy2M = 0x02; |
| // constexpr uint8_t kAseParamPreferredPhyCoded = 0x04; |
| |
| constexpr uint8_t kAseCtpOpcodeConfigureCodec = 0x01; |
| constexpr uint8_t kAseCtpOpcodeConfigureQos = 0x02; |
| constexpr uint8_t kAseCtpOpcodeEnable = 0x03; |
| constexpr uint8_t kAseCtpOpcodeReceiverStartReady = 0x04; |
| constexpr uint8_t kAseCtpOpcodeDisable = 0x05; |
| constexpr uint8_t kAseCtpOpcodeReceiverStopReady = 0x06; |
| // constexpr uint8_t kAseCtpOpcodeUpdateMetadata = 0x07; |
| constexpr uint8_t kAseCtpOpcodeRelease = 0x08; |
| constexpr uint8_t kAseCtpOpcodeMaxVal = kAseCtpOpcodeRelease; |
| |
| } // namespace ascs |
| |
| static RawAddress GetTestAddress(uint8_t index) { |
| return {{0xC0, 0xDE, 0xC0, 0xDE, 0x00, index}}; |
| } |
| |
| static uint8_t ase_id_last_assigned; |
| |
| class MockLeAudioGroupStateMachineCallbacks |
| : public LeAudioGroupStateMachine::Callbacks { |
| public: |
| MockLeAudioGroupStateMachineCallbacks() = default; |
| ~MockLeAudioGroupStateMachineCallbacks() override = default; |
| MOCK_METHOD((void), StatusReportCb, |
| (int group_id, bluetooth::le_audio::GroupStreamStatus status), |
| (override)); |
| MOCK_METHOD((void), OnStateTransitionTimeout, (int group_id), (override)); |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(MockLeAudioGroupStateMachineCallbacks); |
| }; |
| |
| class StateMachineTest : public Test { |
| protected: |
| void SetUp() override { |
| mock_function_count_map.clear(); |
| controller::SetMockControllerInterface(&mock_controller_); |
| bluetooth::manager::SetMockBtmInterface(&btm_interface); |
| gatt::SetMockBtaGattInterface(&gatt_interface); |
| gatt::SetMockBtaGattQueue(&gatt_queue); |
| |
| ase_id_last_assigned = types::ase::kAseIdInvalid; |
| LeAudioGroupStateMachine::Initialize(&mock_callbacks_); |
| |
| // Support 2M Phy |
| ON_CALL(mock_controller_, SupportsBle2mPhy()).WillByDefault(Return(true)); |
| ON_CALL(btm_interface, IsPhy2mSupported(_, _)).WillByDefault(Return(true)); |
| ON_CALL(btm_interface, GetHCIConnHandle(_, _)) |
| .WillByDefault( |
| Invoke([](RawAddress const& remote_bda, tBT_TRANSPORT transport) { |
| return remote_bda.IsEmpty() |
| ? HCI_INVALID_HANDLE |
| : ((uint16_t)(remote_bda.address[0] ^ |
| remote_bda.address[1] ^ |
| remote_bda.address[2])) |
| << 8 || |
| (remote_bda.address[3] ^ remote_bda.address[4] ^ |
| remote_bda.address[5]); |
| })); |
| |
| ON_CALL(gatt_queue, WriteCharacteristic(_, _, _, GATT_WRITE_NO_RSP, _, _)) |
| .WillByDefault(Invoke([this](uint16_t conn_id, uint16_t handle, |
| std::vector<uint8_t> value, |
| tGATT_WRITE_TYPE write_type, |
| GATT_WRITE_OP_CB cb, void* cb_data) { |
| for (auto& dev : le_audio_devices_) { |
| if (dev->conn_id_ == conn_id) { |
| // Control point write handler |
| if (dev->ctp_hdls_.val_hdl == handle) { |
| HandleCtpOperation(dev.get(), value, cb, cb_data); |
| } |
| break; |
| } |
| } |
| })); |
| |
| ConfigureIsoManagerMock(); |
| } |
| |
| void HandleCtpOperation(LeAudioDevice* device, std::vector<uint8_t> value, |
| GATT_WRITE_OP_CB cb, void* cb_data) { |
| auto opcode = value[0]; |
| |
| // Verify against valid opcode range |
| ASSERT_LT(opcode, ascs::kAseCtpOpcodeMaxVal + 1); |
| ASSERT_NE(opcode, 0); |
| |
| if (ase_ctp_handlers[opcode]) |
| ase_ctp_handlers[opcode](device, std::move(value), cb, cb_data); |
| } |
| |
| /* Helper function to make a deterministic (and unique on the entire device) |
| * connection handle for a given cis. |
| */ |
| #define UNIQUE_CIS_CONN_HANDLE(cig_id, cis_index) (cig_id << 8 | cis_index) |
| |
| void ConfigureIsoManagerMock() { |
| iso_manager_ = bluetooth::hci::IsoManager::GetInstance(); |
| ASSERT_NE(iso_manager_, nullptr); |
| iso_manager_->Start(); |
| |
| mock_iso_manager_ = MockIsoManager::GetInstance(); |
| ASSERT_NE(mock_iso_manager_, nullptr); |
| |
| ON_CALL(*mock_iso_manager_, CreateCig) |
| .WillByDefault( |
| [this](uint8_t cig_id, |
| bluetooth::hci::iso_manager::cig_create_params p) { |
| DLOG(INFO) << "CreateCig"; |
| |
| auto& group = le_audio_device_groups_[cig_id]; |
| if (group) { |
| std::vector<uint16_t> conn_handles; |
| // Fake connection ID for each cis in a request |
| for (auto i = 0u; i < p.cis_cfgs.size(); ++i) { |
| conn_handles.push_back(UNIQUE_CIS_CONN_HANDLE(cig_id, i)); |
| } |
| LeAudioGroupStateMachine::Get()->ProcessHciNotifOnCigCreate( |
| group.get(), 0, cig_id, conn_handles); |
| } |
| }); |
| |
| ON_CALL(*mock_iso_manager_, RemoveCig) |
| .WillByDefault([this](uint8_t cig_id) { |
| DLOG(INFO) << "CreateRemove"; |
| |
| auto& group = le_audio_device_groups_[cig_id]; |
| if (group) { |
| // Fake connection ID for each cis in a request |
| LeAudioGroupStateMachine::Get()->ProcessHciNotifOnCigRemove( |
| 0, group.get()); |
| } |
| }); |
| |
| ON_CALL(*mock_iso_manager_, SetupIsoDataPath) |
| .WillByDefault([this](uint16_t conn_handle, |
| bluetooth::hci::iso_manager::iso_data_path_params |
| p) { |
| DLOG(INFO) << "SetupIsoDataPath"; |
| |
| auto dev_it = |
| std::find_if(le_audio_devices_.begin(), le_audio_devices_.end(), |
| [&conn_handle](auto& dev) { |
| auto ases = dev->GetAsesByCisConnHdl(conn_handle); |
| return (ases.sink || ases.source); |
| }); |
| if (dev_it == le_audio_devices_.end()) { |
| DLOG(ERROR) << "Device not found"; |
| return; |
| } |
| |
| for (auto& kv_pair : le_audio_device_groups_) { |
| auto& group = kv_pair.second; |
| if (group->IsDeviceInTheGroup(dev_it->get())) { |
| LeAudioGroupStateMachine::Get()->ProcessHciNotifSetupIsoDataPath( |
| group.get(), dev_it->get(), 0, conn_handle); |
| return; |
| } |
| } |
| }); |
| |
| ON_CALL(*mock_iso_manager_, RemoveIsoDataPath) |
| .WillByDefault([this](uint16_t conn_handle, uint8_t iso_direction) { |
| DLOG(INFO) << "RemoveIsoDataPath"; |
| |
| auto dev_it = |
| std::find_if(le_audio_devices_.begin(), le_audio_devices_.end(), |
| [&conn_handle](auto& dev) { |
| auto ases = dev->GetAsesByCisConnHdl(conn_handle); |
| return (ases.sink || ases.source); |
| }); |
| if (dev_it == le_audio_devices_.end()) { |
| DLOG(ERROR) << "Device not found"; |
| return; |
| } |
| |
| for (auto& kv_pair : le_audio_device_groups_) { |
| auto& group = kv_pair.second; |
| if (group->IsDeviceInTheGroup(dev_it->get())) { |
| LeAudioGroupStateMachine::Get()->ProcessHciNotifRemoveIsoDataPath( |
| group.get(), dev_it->get(), 0, conn_handle); |
| return; |
| } |
| } |
| }); |
| |
| ON_CALL(*mock_iso_manager_, EstablishCis) |
| .WillByDefault([this](bluetooth::hci::iso_manager::cis_establish_params |
| conn_params) { |
| DLOG(INFO) << "EstablishCis"; |
| |
| for (auto& pair : conn_params.conn_pairs) { |
| auto dev_it = std::find_if( |
| le_audio_devices_.begin(), le_audio_devices_.end(), |
| [&pair](auto& dev) { |
| auto ases = dev->GetAsesByCisConnHdl(pair.cis_conn_handle); |
| return (ases.sink || ases.source); |
| }); |
| if (dev_it == le_audio_devices_.end()) { |
| DLOG(ERROR) << "Device not found"; |
| return; |
| } |
| |
| for (auto& kv_pair : le_audio_device_groups_) { |
| auto& group = kv_pair.second; |
| if (group->IsDeviceInTheGroup(dev_it->get())) { |
| bluetooth::hci::iso_manager::cis_establish_cmpl_evt evt; |
| |
| // Fill proper values if needed |
| evt.status = 0x00; |
| evt.cig_id = group->group_id_; |
| evt.cis_conn_hdl = pair.cis_conn_handle; |
| evt.cig_sync_delay = 0; |
| evt.cis_sync_delay = 0; |
| evt.trans_lat_mtos = 0; |
| evt.trans_lat_stom = 0; |
| evt.phy_mtos = 0; |
| evt.phy_stom = 0; |
| evt.nse = 0; |
| evt.bn_mtos = 0; |
| evt.bn_stom = 0; |
| evt.ft_mtos = 0; |
| evt.ft_stom = 0; |
| evt.max_pdu_mtos = 0; |
| evt.max_pdu_stom = 0; |
| evt.iso_itv = 0; |
| |
| LeAudioGroupStateMachine::Get()->ProcessHciNotifCisEstablished( |
| group.get(), dev_it->get(), &evt); |
| break; |
| } |
| } |
| } |
| }); |
| |
| ON_CALL(*mock_iso_manager_, DisconnectCis) |
| .WillByDefault([this](uint16_t cis_handle, uint8_t reason) { |
| DLOG(INFO) << "DisconnectCis"; |
| |
| auto dev_it = |
| std::find_if(le_audio_devices_.begin(), le_audio_devices_.end(), |
| [&cis_handle](auto& dev) { |
| auto ases = dev->GetAsesByCisConnHdl(cis_handle); |
| return (ases.sink || ases.source); |
| }); |
| if (dev_it == le_audio_devices_.end()) { |
| DLOG(ERROR) << "Device not found"; |
| return; |
| } |
| |
| for (auto& kv_pair : le_audio_device_groups_) { |
| auto& group = kv_pair.second; |
| if (group->IsDeviceInTheGroup(dev_it->get())) { |
| bluetooth::hci::iso_manager::cis_disconnected_evt evt{ |
| .reason = reason, |
| .cig_id = static_cast<uint8_t>(group->group_id_), |
| .cis_conn_hdl = cis_handle, |
| }; |
| LeAudioGroupStateMachine::Get()->ProcessHciNotifCisDisconnected( |
| group.get(), dev_it->get(), &evt); |
| return; |
| } |
| } |
| }); |
| } |
| |
| void TearDown() override { |
| iso_manager_->Stop(); |
| mock_iso_manager_ = nullptr; |
| |
| gatt::SetMockBtaGattQueue(nullptr); |
| gatt::SetMockBtaGattInterface(nullptr); |
| bluetooth::manager::SetMockBtmInterface(nullptr); |
| controller::SetMockControllerInterface(nullptr); |
| |
| for (auto i = 0u; i <= ascs::kAseCtpOpcodeMaxVal; ++i) |
| ase_ctp_handlers[i] = nullptr; |
| |
| le_audio_devices_.clear(); |
| cached_codec_configuration_map_.clear(); |
| LeAudioGroupStateMachine::Cleanup(); |
| } |
| |
| std::shared_ptr<LeAudioDevice> PrepareConnectedDevice(uint8_t id, |
| bool first_connection, |
| uint8_t num_ase_snk, |
| uint8_t num_ase_src) { |
| auto leAudioDevice = |
| std::make_shared<LeAudioDevice>(GetTestAddress(id), first_connection); |
| leAudioDevice->conn_id_ = id; |
| |
| uint16_t attr_handle = ATTR_HANDLE_ASCS_POOL_START; |
| leAudioDevice->snk_audio_locations_hdls_.val_hdl = attr_handle++; |
| leAudioDevice->snk_audio_locations_hdls_.ccc_hdl = attr_handle++; |
| leAudioDevice->src_audio_locations_hdls_.val_hdl = attr_handle++; |
| leAudioDevice->src_audio_locations_hdls_.ccc_hdl = attr_handle++; |
| leAudioDevice->audio_avail_hdls_.val_hdl = attr_handle++; |
| leAudioDevice->audio_avail_hdls_.ccc_hdl = attr_handle++; |
| leAudioDevice->audio_supp_cont_hdls_.val_hdl = attr_handle++; |
| leAudioDevice->audio_supp_cont_hdls_.ccc_hdl = attr_handle++; |
| leAudioDevice->ctp_hdls_.val_hdl = attr_handle++; |
| leAudioDevice->ctp_hdls_.ccc_hdl = attr_handle++; |
| |
| // Add some Sink ASEs |
| while (num_ase_snk) { |
| types::ase ase(0, 0, 0x01); |
| ase.hdls.val_hdl = attr_handle++; |
| ase.hdls.ccc_hdl = attr_handle++; |
| |
| leAudioDevice->ases_.emplace_back(std::move(ase)); |
| num_ase_snk--; |
| } |
| |
| // Add some Source ASEs |
| while (num_ase_src) { |
| types::ase ase(0, 0, 0x02); |
| ase.hdls.val_hdl = attr_handle++; |
| ase.hdls.ccc_hdl = attr_handle++; |
| |
| leAudioDevice->ases_.emplace_back(std::move(ase)); |
| num_ase_src--; |
| } |
| |
| le_audio_devices_.push_back(leAudioDevice); |
| |
| return std::move(leAudioDevice); |
| } |
| |
| LeAudioDeviceGroup* GroupTheDevice( |
| int group_id, const std::shared_ptr<LeAudioDevice>& leAudioDevice) { |
| if (le_audio_device_groups_.count(group_id) == 0) { |
| le_audio_device_groups_[group_id] = |
| std::make_unique<LeAudioDeviceGroup>(group_id); |
| } |
| |
| auto& group = le_audio_device_groups_[group_id]; |
| |
| group->AddNode(leAudioDevice); |
| if (group->IsEmpty()) return nullptr; |
| |
| return &(*group); |
| } |
| |
| static void InjectAseStateNotification(types::ase* ase, LeAudioDevice* device, |
| LeAudioDeviceGroup* group, |
| uint8_t new_state, |
| void* new_state_params) { |
| // Prepare additional params |
| switch (new_state) { |
| case ascs::kAseStateCodecConfigured: { |
| client_parser::ascs::ase_codec_configured_state_params* conf = |
| static_cast< |
| client_parser::ascs::ase_codec_configured_state_params*>( |
| new_state_params); |
| std::vector<uint8_t> notif_value(25 + conf->codec_spec_conf.size()); |
| auto* p = notif_value.data(); |
| |
| UINT8_TO_STREAM(p, ase->id == types::ase::kAseIdInvalid |
| ? ++ase_id_last_assigned |
| : ase->id); |
| UINT8_TO_STREAM(p, new_state); |
| |
| UINT8_TO_STREAM(p, conf->framing); |
| UINT8_TO_STREAM(p, conf->preferred_phy); |
| UINT8_TO_STREAM(p, conf->preferred_retrans_nb); |
| UINT16_TO_STREAM(p, conf->max_transport_latency); |
| UINT24_TO_STREAM(p, conf->pres_delay_min); |
| UINT24_TO_STREAM(p, conf->pres_delay_max); |
| UINT24_TO_STREAM(p, conf->preferred_pres_delay_min); |
| UINT24_TO_STREAM(p, conf->preferred_pres_delay_max); |
| |
| // CodecID: |
| UINT8_TO_STREAM(p, conf->codec_id.coding_format); |
| UINT16_TO_STREAM(p, conf->codec_id.vendor_company_id); |
| UINT16_TO_STREAM(p, conf->codec_id.vendor_codec_id); |
| |
| // Codec Spec. Conf. Length and Data |
| UINT8_TO_STREAM(p, conf->codec_spec_conf.size()); |
| memcpy(p, conf->codec_spec_conf.data(), conf->codec_spec_conf.size()); |
| |
| LeAudioGroupStateMachine::Get()->ProcessGattNotifEvent( |
| notif_value.data(), notif_value.size(), ase, device, group); |
| } break; |
| |
| case ascs::kAseStateQoSConfigured: { |
| client_parser::ascs::ase_qos_configured_state_params* conf = |
| static_cast<client_parser::ascs::ase_qos_configured_state_params*>( |
| new_state_params); |
| std::vector<uint8_t> notif_value(17); |
| auto* p = notif_value.data(); |
| |
| // Prepare header |
| UINT8_TO_STREAM(p, ase->id); |
| UINT8_TO_STREAM(p, new_state); |
| |
| UINT8_TO_STREAM(p, conf->cig_id); |
| UINT8_TO_STREAM(p, conf->cis_id); |
| UINT24_TO_STREAM(p, conf->sdu_interval); |
| UINT8_TO_STREAM(p, conf->framing); |
| UINT8_TO_STREAM(p, conf->phy); |
| UINT16_TO_STREAM(p, conf->max_sdu); |
| UINT8_TO_STREAM(p, conf->retrans_nb); |
| UINT16_TO_STREAM(p, conf->max_transport_latency); |
| UINT24_TO_STREAM(p, conf->pres_delay); |
| |
| LeAudioGroupStateMachine::Get()->ProcessGattNotifEvent( |
| notif_value.data(), notif_value.size(), ase, device, group); |
| } break; |
| |
| case ascs::kAseStateEnabling: |
| // fall-through |
| case ascs::kAseStateStreaming: |
| // fall-through |
| case ascs::kAseStateDisabling: { |
| client_parser::ascs::ase_transient_state_params* params = |
| static_cast<client_parser::ascs::ase_transient_state_params*>( |
| new_state_params); |
| std::vector<uint8_t> notif_value(5 + params->metadata.size()); |
| auto* p = notif_value.data(); |
| |
| // Prepare header |
| UINT8_TO_STREAM(p, ase->id); |
| UINT8_TO_STREAM(p, new_state); |
| |
| UINT8_TO_STREAM(p, group->group_id_); |
| UINT8_TO_STREAM(p, ase->cis_id); |
| UINT8_TO_STREAM(p, params->metadata.size()); |
| memcpy(p, params->metadata.data(), params->metadata.size()); |
| |
| LeAudioGroupStateMachine::Get()->ProcessGattNotifEvent( |
| notif_value.data(), notif_value.size(), ase, device, group); |
| } break; |
| |
| case ascs::kAseStateReleasing: |
| // fall-through |
| case ascs::kAseStateIdle: { |
| std::vector<uint8_t> notif_value(2); |
| auto* p = notif_value.data(); |
| |
| // Prepare header |
| UINT8_TO_STREAM(p, ase->id == types::ase::kAseIdInvalid |
| ? ++ase_id_last_assigned |
| : ase->id); |
| UINT8_TO_STREAM(p, new_state); |
| |
| LeAudioGroupStateMachine::Get()->ProcessGattNotifEvent( |
| notif_value.data(), notif_value.size(), ase, device, group); |
| } break; |
| |
| default: |
| break; |
| }; |
| } |
| |
| static void InsertPacRecord( |
| std::vector<types::acs_ac_record>& recs, |
| uint16_t sampling_frequencies_bitfield, |
| uint8_t supported_frame_durations_bitfield, |
| uint8_t audio_channel_count_bitfield, |
| uint16_t supported_octets_per_codec_frame_min, |
| uint16_t supported_octets_per_codec_frame_max, |
| uint8_t coding_format = codec_specific::kLc3CodingFormat, |
| uint16_t vendor_company_id = 0x0000, uint16_t vendor_codec_id = 0x0000, |
| std::vector<uint8_t> metadata = {}) { |
| recs.push_back({ |
| .codec_id = |
| { |
| .coding_format = coding_format, |
| .vendor_company_id = vendor_company_id, |
| .vendor_codec_id = vendor_codec_id, |
| }, |
| .codec_spec_caps = types::LeAudioLtvMap({ |
| {codec_specific::kCapTypeSupportedSamplingFrequencies, |
| {(uint8_t)(sampling_frequencies_bitfield), |
| (uint8_t)(sampling_frequencies_bitfield >> 8)}}, |
| {codec_specific::kCapTypeSupportedFrameDurations, |
| {supported_frame_durations_bitfield}}, |
| {codec_specific::kCapTypeAudioChannelCount, |
| {audio_channel_count_bitfield}}, |
| {codec_specific::kCapTypeSupportedOctetsPerCodecFrame, |
| { |
| // Min |
| (uint8_t)(supported_octets_per_codec_frame_min), |
| (uint8_t)(supported_octets_per_codec_frame_min >> 8), |
| // Max |
| (uint8_t)(supported_octets_per_codec_frame_max), |
| (uint8_t)(supported_octets_per_codec_frame_max >> 8), |
| }}, |
| }), |
| .metadata = std::move(metadata), |
| }); |
| } |
| |
| static void InjectInitialIdleNotification(LeAudioDeviceGroup* group) { |
| for (auto* device = group->GetFirstDevice(); device != nullptr; |
| device = group->GetNextDevice(device)) { |
| for (auto& ase : device->ases_) { |
| InjectAseStateNotification(&ase, device, group, ascs::kAseStateIdle, |
| nullptr); |
| } |
| } |
| } |
| |
| void MultipleTestDevicePrepare(int leaudio_group_id, uint16_t context_type, |
| uint16_t device_cnt, |
| bool insert_default_pac_records = true) { |
| // Prepare fake connected device group |
| bool first_connections = true; |
| int total_devices = device_cnt; |
| le_audio::LeAudioDeviceGroup* group = nullptr; |
| |
| uint8_t num_ase_snk; |
| uint8_t num_ase_src; |
| switch (context_type) { |
| case kContextTypeRingtone: |
| num_ase_snk = 1; |
| num_ase_src = 0; |
| break; |
| |
| case kContextTypeMedia: |
| num_ase_snk = 2; |
| num_ase_src = 0; |
| break; |
| |
| case kContextTypeConversational: |
| num_ase_snk = 1; |
| num_ase_src = 1; |
| break; |
| |
| default: |
| ASSERT_TRUE(false); |
| } |
| |
| while (device_cnt) { |
| auto leAudioDevice = PrepareConnectedDevice( |
| device_cnt--, first_connections, num_ase_snk, num_ase_src); |
| |
| if (insert_default_pac_records) { |
| uint16_t attr_handle = ATTR_HANDLE_PACS_POOL_START; |
| |
| /* As per spec, unspecified shall be supported */ |
| types::AudioContexts snk_context_type = kContextTypeUnspecified; |
| types::AudioContexts src_context_type = kContextTypeUnspecified; |
| |
| // Prepare Sink Published Audio Capability records |
| if ((context_type & kContextTypeRingtone) || |
| (context_type & kContextTypeMedia) || |
| (context_type & kContextTypeConversational)) { |
| // Set target ASE configurations |
| std::vector<types::acs_ac_record> pac_recs; |
| |
| InsertPacRecord(pac_recs, |
| codec_specific::kCapSamplingFrequency16000Hz, |
| codec_specific::kCapFrameDuration10ms | |
| codec_specific::kCapFrameDuration7p5ms | |
| codec_specific::kCapFrameDuration10msPreferred, |
| 0b00000001, 30, 120); |
| |
| types::hdl_pair handle_pair; |
| handle_pair.val_hdl = attr_handle++; |
| handle_pair.ccc_hdl = attr_handle++; |
| |
| leAudioDevice->snk_pacs_.emplace_back( |
| std::make_tuple(std::move(handle_pair), pac_recs)); |
| |
| snk_context_type |= context_type; |
| leAudioDevice->snk_audio_locations_ = |
| ::le_audio::codec_spec_conf::kLeAudioLocationFrontLeft | |
| ::le_audio::codec_spec_conf::kLeAudioLocationFrontRight; |
| } |
| |
| // Prepare Source Published Audio Capability records |
| if (context_type & kContextTypeConversational) { |
| // Set target ASE configurations |
| std::vector<types::acs_ac_record> pac_recs; |
| |
| InsertPacRecord(pac_recs, |
| codec_specific::kCapSamplingFrequency16000Hz, |
| codec_specific::kCapFrameDuration10ms | |
| codec_specific::kCapFrameDuration7p5ms | |
| codec_specific::kCapFrameDuration10msPreferred, |
| 0b00000001, 30, 120); |
| |
| types::hdl_pair handle_pair; |
| handle_pair.val_hdl = attr_handle++; |
| handle_pair.ccc_hdl = attr_handle++; |
| |
| leAudioDevice->src_pacs_.emplace_back( |
| std::make_tuple(std::move(handle_pair), pac_recs)); |
| src_context_type |= kContextTypeConversational; |
| |
| leAudioDevice->src_audio_locations_ = |
| ::le_audio::codec_spec_conf::kLeAudioLocationFrontLeft; |
| } |
| |
| leAudioDevice->SetSupportedContexts(snk_context_type, src_context_type); |
| leAudioDevice->SetAvailableContexts(snk_context_type, src_context_type); |
| } |
| |
| group = GroupTheDevice(leaudio_group_id, std::move(leAudioDevice)); |
| } |
| |
| /* Stimulate update of active context map */ |
| types::AudioContexts type_set = static_cast<uint16_t>(context_type); |
| group->UpdateActiveContextsMap(type_set); |
| |
| ASSERT_NE(group, nullptr); |
| ASSERT_EQ(group->Size(), total_devices); |
| } |
| |
| LeAudioDeviceGroup* PrepareSingleTestDeviceGroup(int leaudio_group_id, |
| uint16_t context_type, |
| uint16_t device_cnt = 1) { |
| MultipleTestDevicePrepare(leaudio_group_id, context_type, device_cnt); |
| return le_audio_device_groups_.count(leaudio_group_id) |
| ? le_audio_device_groups_[leaudio_group_id].get() |
| : nullptr; |
| } |
| |
| void PrepareConfigureCodecHandler(LeAudioDeviceGroup* group, |
| int verify_ase_count = 0, |
| bool caching = false) { |
| ase_ctp_handlers[ascs::kAseCtpOpcodeConfigureCodec] = |
| [group, verify_ase_count, caching, this]( |
| LeAudioDevice* device, std::vector<uint8_t> value, |
| GATT_WRITE_OP_CB cb, void* cb_data) { |
| auto num_ase = value[1]; |
| |
| // Verify ase count if needed |
| if (verify_ase_count) ASSERT_EQ(verify_ase_count, num_ase); |
| |
| // Inject Configured ASE state notification for each requested ASE |
| auto* ase_p = &value[2]; |
| for (auto i = 0u; i < num_ase; ++i) { |
| client_parser::ascs::ase_codec_configured_state_params |
| codec_configured_state_params; |
| |
| /* Check if this is a valid ASE ID */ |
| auto ase_id = *ase_p++; |
| auto it = std::find_if( |
| device->ases_.begin(), device->ases_.end(), |
| [ase_id](auto& ase) { return (ase.id == ase_id); }); |
| ASSERT_NE(it, device->ases_.end()); |
| const auto ase = &(*it); |
| |
| // Skip target latency param |
| ase_p++; |
| |
| codec_configured_state_params.preferred_phy = *ase_p++; |
| codec_configured_state_params.codec_id.coding_format = ase_p[0]; |
| codec_configured_state_params.codec_id.vendor_company_id = |
| (uint16_t)(ase_p[1] << 8 | ase_p[2]), |
| codec_configured_state_params.codec_id.vendor_codec_id = |
| (uint16_t)(ase_p[3] << 8 | ase_p[4]), |
| ase_p += 5; |
| |
| auto codec_spec_param_len = *ase_p++; |
| auto num_handled_bytes = ase_p - value.data(); |
| codec_configured_state_params.codec_spec_conf = |
| std::vector<uint8_t>( |
| value.begin() + num_handled_bytes, |
| value.begin() + num_handled_bytes + codec_spec_param_len); |
| ase_p += codec_spec_param_len; |
| |
| // Some initial QoS settings |
| codec_configured_state_params.framing = |
| ascs::kAseParamFramingUnframedSupported; |
| codec_configured_state_params.preferred_retrans_nb = 0x04; |
| codec_configured_state_params.max_transport_latency = 0x0005; |
| codec_configured_state_params.pres_delay_min = 0xABABAB; |
| codec_configured_state_params.pres_delay_max = 0xCDCDCD; |
| codec_configured_state_params.preferred_pres_delay_min = |
| types::kPresDelayNoPreference; |
| codec_configured_state_params.preferred_pres_delay_max = |
| types::kPresDelayNoPreference; |
| |
| if (caching) { |
| cached_codec_configuration_map_[ase_id] = |
| codec_configured_state_params; |
| } |
| InjectAseStateNotification(ase, device, group, |
| ascs::kAseStateCodecConfigured, |
| &codec_configured_state_params); |
| } |
| }; |
| } |
| |
| void PrepareConfigureQosHandler(LeAudioDeviceGroup* group, |
| int verify_ase_count = 0) { |
| ase_ctp_handlers[ascs::kAseCtpOpcodeConfigureQos] = |
| [group, verify_ase_count](LeAudioDevice* device, |
| std::vector<uint8_t> value, |
| GATT_WRITE_OP_CB cb, void* cb_data) { |
| auto num_ase = value[1]; |
| |
| // Verify ase count if needed |
| if (verify_ase_count) ASSERT_EQ(verify_ase_count, num_ase); |
| |
| // Inject Configured QoS state notification for each requested ASE |
| auto* ase_p = &value[2]; |
| for (auto i = 0u; i < num_ase; ++i) { |
| client_parser::ascs::ase_qos_configured_state_params |
| qos_configured_state_params; |
| |
| /* Check if this is a valid ASE ID */ |
| auto ase_id = *ase_p++; |
| auto it = std::find_if( |
| device->ases_.begin(), device->ases_.end(), |
| [ase_id](auto& ase) { return (ase.id == ase_id); }); |
| ASSERT_NE(it, device->ases_.end()); |
| const auto ase = &(*it); |
| |
| qos_configured_state_params.cig_id = *ase_p++; |
| qos_configured_state_params.cis_id = *ase_p++; |
| |
| qos_configured_state_params.sdu_interval = |
| (uint32_t)((ase_p[0] << 16) | (ase_p[1] << 8) | ase_p[2]); |
| ase_p += 3; |
| |
| qos_configured_state_params.framing = *ase_p++; |
| qos_configured_state_params.phy = *ase_p++; |
| qos_configured_state_params.max_sdu = |
| (uint16_t)((ase_p[0] << 8) | ase_p[1]); |
| ase_p += 2; |
| |
| qos_configured_state_params.retrans_nb = *ase_p++; |
| qos_configured_state_params.max_transport_latency = |
| (uint16_t)((ase_p[0] << 8) | ase_p[1]); |
| ase_p += 2; |
| |
| qos_configured_state_params.pres_delay = |
| (uint16_t)((ase_p[0] << 16) | (ase_p[1] << 8) | ase_p[2]); |
| ase_p += 3; |
| |
| InjectAseStateNotification(ase, device, group, |
| ascs::kAseStateQoSConfigured, |
| &qos_configured_state_params); |
| } |
| }; |
| } |
| |
| void PrepareEnableHandler(LeAudioDeviceGroup* group, int verify_ase_count = 0, |
| bool inject_enabling = true) { |
| ase_ctp_handlers[ascs::kAseCtpOpcodeEnable] = |
| [group, verify_ase_count, inject_enabling]( |
| LeAudioDevice* device, std::vector<uint8_t> value, |
| GATT_WRITE_OP_CB cb, void* cb_data) { |
| auto num_ase = value[1]; |
| |
| // Verify ase count if needed |
| if (verify_ase_count) ASSERT_EQ(verify_ase_count, num_ase); |
| |
| // Inject Streaming ASE state notification for each requested ASE |
| auto* ase_p = &value[2]; |
| for (auto i = 0u; i < num_ase; ++i) { |
| /* Check if this is a valid ASE ID */ |
| auto ase_id = *ase_p++; |
| auto it = std::find_if( |
| device->ases_.begin(), device->ases_.end(), |
| [ase_id](auto& ase) { return (ase.id == ase_id); }); |
| ASSERT_NE(it, device->ases_.end()); |
| const auto ase = &(*it); |
| |
| auto meta_len = *ase_p++; |
| auto num_handled_bytes = ase_p - value.data(); |
| ase_p += num_handled_bytes; |
| |
| client_parser::ascs::ase_transient_state_params enable_params = { |
| .metadata = std::vector<uint8_t>( |
| value.begin() + num_handled_bytes, |
| value.begin() + num_handled_bytes + meta_len)}; |
| |
| // Server does the 'ReceiverStartReady' on its own - goes to |
| // Streaming, when in Sink role |
| if (ase->direction & le_audio::types::kLeAudioDirectionSink) { |
| if (inject_enabling) |
| InjectAseStateNotification(ase, device, group, |
| ascs::kAseStateEnabling, |
| &enable_params); |
| InjectAseStateNotification( |
| ase, device, group, ascs::kAseStateStreaming, &enable_params); |
| } else { |
| InjectAseStateNotification( |
| ase, device, group, ascs::kAseStateEnabling, &enable_params); |
| } |
| } |
| }; |
| } |
| |
| void PrepareDisableHandler(LeAudioDeviceGroup* group, |
| int verify_ase_count = 0) { |
| ase_ctp_handlers[ascs::kAseCtpOpcodeDisable] = |
| [group, verify_ase_count](LeAudioDevice* device, |
| std::vector<uint8_t> value, |
| GATT_WRITE_OP_CB cb, void* cb_data) { |
| auto num_ase = value[1]; |
| |
| // Verify ase count if needed |
| if (verify_ase_count) ASSERT_EQ(verify_ase_count, num_ase); |
| ASSERT_EQ(value.size(), 2ul + num_ase); |
| |
| // Inject Disabling & QoS Conf. ASE state notification for each ASE |
| auto* ase_p = &value[2]; |
| for (auto i = 0u; i < num_ase; ++i) { |
| /* Check if this is a valid ASE ID */ |
| auto ase_id = *ase_p++; |
| auto it = std::find_if( |
| device->ases_.begin(), device->ases_.end(), |
| [ase_id](auto& ase) { return (ase.id == ase_id); }); |
| ASSERT_NE(it, device->ases_.end()); |
| const auto ase = &(*it); |
| |
| // The Disabling state is present for Source ASE |
| if (ase->direction & le_audio::types::kLeAudioDirectionSource) { |
| client_parser::ascs::ase_transient_state_params disabling_params = |
| {.metadata = {}}; |
| InjectAseStateNotification(ase, device, group, |
| ascs::kAseStateDisabling, |
| &disabling_params); |
| } |
| |
| // Server does the 'ReceiverStopReady' on its own - goes to |
| // Streaming, when in Sink role |
| if (ase->direction & le_audio::types::kLeAudioDirectionSink) { |
| // FIXME: For now our fake peer does not remember qos params |
| client_parser::ascs::ase_qos_configured_state_params |
| qos_configured_state_params; |
| InjectAseStateNotification(ase, device, group, |
| ascs::kAseStateQoSConfigured, |
| &qos_configured_state_params); |
| } |
| } |
| }; |
| } |
| |
| void PrepareReceiverStartReady(LeAudioDeviceGroup* group, |
| int verify_ase_count = 0) { |
| ase_ctp_handlers[ascs::kAseCtpOpcodeReceiverStartReady] = |
| [group, verify_ase_count](LeAudioDevice* device, |
| std::vector<uint8_t> value, |
| GATT_WRITE_OP_CB cb, void* cb_data) { |
| auto num_ase = value[1]; |
| |
| // Verify ase count if needed |
| if (verify_ase_count) ASSERT_EQ(verify_ase_count, num_ase); |
| |
| // Inject Streaming ASE state notification for each Source ASE |
| auto* ase_p = &value[2]; |
| for (auto i = 0u; i < num_ase; ++i) { |
| /* Check if this is a valid ASE ID */ |
| auto ase_id = *ase_p++; |
| auto it = std::find_if( |
| device->ases_.begin(), device->ases_.end(), |
| [ase_id](auto& ase) { return (ase.id == ase_id); }); |
| ASSERT_NE(it, device->ases_.end()); |
| |
| // Once we did the 'ReceiverStartReady' the server goes to |
| // Streaming, when in Source role |
| auto meta_len = *ase_p++; |
| auto num_handled_bytes = ase_p - value.data(); |
| ase_p += num_handled_bytes; |
| |
| const auto& ase = &(*it); |
| client_parser::ascs::ase_transient_state_params enable_params = { |
| .metadata = std::vector<uint8_t>( |
| value.begin() + num_handled_bytes, |
| value.begin() + num_handled_bytes + meta_len)}; |
| InjectAseStateNotification( |
| ase, device, group, ascs::kAseStateStreaming, &enable_params); |
| } |
| }; |
| } |
| |
| void PrepareReceiverStopReady(LeAudioDeviceGroup* group, |
| int verify_ase_count = 0) { |
| ase_ctp_handlers[ascs::kAseCtpOpcodeReceiverStopReady] = |
| [group, verify_ase_count](LeAudioDevice* device, |
| std::vector<uint8_t> value, |
| GATT_WRITE_OP_CB cb, void* cb_data) { |
| auto num_ase = value[1]; |
| |
| // Verify ase count if needed |
| if (verify_ase_count) ASSERT_EQ(verify_ase_count, num_ase); |
| |
| // Inject QoS configured ASE state notification for each Source ASE |
| auto* ase_p = &value[2]; |
| for (auto i = 0u; i < num_ase; ++i) { |
| /* Check if this is a valid ASE ID */ |
| auto ase_id = *ase_p++; |
| auto it = std::find_if( |
| device->ases_.begin(), device->ases_.end(), |
| [ase_id](auto& ase) { return (ase.id == ase_id); }); |
| ASSERT_NE(it, device->ases_.end()); |
| |
| const auto& ase = &(*it); |
| |
| // FIXME: For now our fake peer does not remember qos params |
| client_parser::ascs::ase_qos_configured_state_params |
| qos_configured_state_params; |
| InjectAseStateNotification(ase, device, group, |
| ascs::kAseStateQoSConfigured, |
| &qos_configured_state_params); |
| } |
| }; |
| } |
| |
| void PrepareReleaseHandler(LeAudioDeviceGroup* group, |
| int verify_ase_count = 0) { |
| ase_ctp_handlers[ascs::kAseCtpOpcodeRelease] = |
| [group, verify_ase_count, this](LeAudioDevice* device, |
| std::vector<uint8_t> value, |
| GATT_WRITE_OP_CB cb, void* cb_data) { |
| auto num_ase = value[1]; |
| |
| // Verify ase count if needed |
| if (verify_ase_count) ASSERT_EQ(verify_ase_count, num_ase); |
| ASSERT_EQ(value.size(), 2ul + num_ase); |
| |
| // Inject Releasing & Idle ASE state notification for each ASE |
| auto* ase_p = &value[2]; |
| for (auto i = 0u; i < num_ase; ++i) { |
| /* Check if this is a valid ASE ID */ |
| auto ase_id = *ase_p++; |
| auto it = std::find_if( |
| device->ases_.begin(), device->ases_.end(), |
| [ase_id](auto& ase) { return (ase.id == ase_id); }); |
| ASSERT_NE(it, device->ases_.end()); |
| const auto ase = &(*it); |
| |
| InjectAseStateNotification(ase, device, group, |
| ascs::kAseStateReleasing, nullptr); |
| |
| /* Check if codec configuration is cached */ |
| if (cached_codec_configuration_map_.count(ase_id) > 0) { |
| InjectAseStateNotification( |
| ase, device, group, ascs::kAseStateCodecConfigured, |
| &cached_codec_configuration_map_[ase_id]); |
| } else { |
| // Release - no caching |
| InjectAseStateNotification(ase, device, group, |
| ascs::kAseStateIdle, nullptr); |
| } |
| } |
| }; |
| } |
| |
| controller::MockControllerInterface mock_controller_; |
| bluetooth::manager::MockBtmInterface btm_interface; |
| gatt::MockBtaGattInterface gatt_interface; |
| gatt::MockBtaGattQueue gatt_queue; |
| |
| bluetooth::hci::IsoManager* iso_manager_; |
| MockIsoManager* mock_iso_manager_; |
| |
| std::function<void(LeAudioDevice* device, std::vector<uint8_t> value, |
| GATT_WRITE_OP_CB cb, void* cb_data)> |
| ase_ctp_handlers[ascs::kAseCtpOpcodeMaxVal + 1] = {nullptr}; |
| std::map<int, client_parser::ascs::ase_codec_configured_state_params> |
| cached_codec_configuration_map_; |
| |
| MockLeAudioGroupStateMachineCallbacks mock_callbacks_; |
| std::vector<std::shared_ptr<LeAudioDevice>> le_audio_devices_; |
| std::map<uint8_t, std::unique_ptr<LeAudioDeviceGroup>> |
| le_audio_device_groups_; |
| }; |
| |
| TEST_F(StateMachineTest, testInit) { |
| ASSERT_NE(LeAudioGroupStateMachine::Get(), nullptr); |
| } |
| |
| TEST_F(StateMachineTest, testCleanup) { |
| ASSERT_NE(LeAudioGroupStateMachine::Get(), nullptr); |
| LeAudioGroupStateMachine::Cleanup(); |
| EXPECT_DEATH(LeAudioGroupStateMachine::Get(), ""); |
| } |
| |
| TEST_F(StateMachineTest, testConfigureCodecSingle) { |
| const auto context_type = kContextTypeRingtone; |
| const int leaudio_group_id = 2; |
| |
| // Prepare fake connected device group |
| auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type); |
| |
| /* Since we prepared device with Ringtone context in mind, only one ASE |
| * should have been configured. |
| */ |
| auto* leAudioDevice = group->GetFirstDevice(); |
| PrepareConfigureCodecHandler(group, 1); |
| |
| // Start the configuration and stream Media content |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(2); |
| |
| InjectInitialIdleNotification(group); |
| |
| ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type))); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED); |
| } |
| |
| TEST_F(StateMachineTest, testConfigureCodecMulti) { |
| const auto context_type = kContextTypeMedia; |
| const auto leaudio_group_id = 2; |
| const auto num_devices = 2; |
| |
| // Prepare multiple fake connected devices in a group |
| auto* group = |
| PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices); |
| ASSERT_EQ(group->Size(), num_devices); |
| |
| PrepareConfigureCodecHandler(group); |
| |
| auto expected_devices_written = 0; |
| auto* leAudioDevice = group->GetFirstDevice(); |
| while (leAudioDevice) { |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(leAudioDevice->conn_id_, |
| leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(AtLeast(1)); |
| expected_devices_written++; |
| leAudioDevice = group->GetNextDevice(leAudioDevice); |
| } |
| ASSERT_EQ(expected_devices_written, num_devices); |
| |
| InjectInitialIdleNotification(group); |
| |
| // Start the configuration and stream the content |
| ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type))); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED); |
| } |
| |
| TEST_F(StateMachineTest, testConfigureQosSingle) { |
| const auto context_type = kContextTypeRingtone; |
| const int leaudio_group_id = 3; |
| |
| // Prepare fake connected device group |
| auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type); |
| |
| /* Since we prepared device with Ringtone context in mind, only one ASE |
| * should have been configured. |
| */ |
| auto* leAudioDevice = group->GetFirstDevice(); |
| PrepareConfigureCodecHandler(group, 1); |
| PrepareConfigureQosHandler(group, 1); |
| |
| // Start the configuration and stream Media content |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(3); |
| |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); |
| |
| InjectInitialIdleNotification(group); |
| |
| ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type))); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED); |
| } |
| |
| TEST_F(StateMachineTest, testConfigureQosMultiple) { |
| const auto context_type = kContextTypeMedia; |
| const auto leaudio_group_id = 3; |
| const auto num_devices = 2; |
| |
| // Prepare multiple fake connected devices in a group |
| auto* group = |
| PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices); |
| ASSERT_EQ(group->Size(), num_devices); |
| |
| PrepareConfigureCodecHandler(group); |
| PrepareConfigureQosHandler(group); |
| |
| auto* leAudioDevice = group->GetFirstDevice(); |
| auto expected_devices_written = 0; |
| while (leAudioDevice) { |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(leAudioDevice->conn_id_, |
| leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(AtLeast(2)); |
| expected_devices_written++; |
| leAudioDevice = group->GetNextDevice(leAudioDevice); |
| } |
| ASSERT_EQ(expected_devices_written, num_devices); |
| |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); |
| |
| InjectInitialIdleNotification(group); |
| |
| // Start the configuration and stream Media content |
| ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type))); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED); |
| } |
| |
| TEST_F(StateMachineTest, testStreamSingle) { |
| const auto context_type = kContextTypeRingtone; |
| const int leaudio_group_id = 4; |
| |
| // Prepare fake connected device group |
| auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type); |
| |
| /* Since we prepared device with Ringtone context in mind, only one ASE |
| * should have been configured. |
| */ |
| PrepareConfigureCodecHandler(group, 1); |
| PrepareConfigureQosHandler(group, 1); |
| PrepareEnableHandler(group, 1); |
| |
| auto* leAudioDevice = group->GetFirstDevice(); |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(3); |
| |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); |
| |
| InjectInitialIdleNotification(group); |
| |
| // Validate GroupStreamStatus |
| EXPECT_CALL( |
| mock_callbacks_, |
| StatusReportCb(leaudio_group_id, |
| bluetooth::le_audio::GroupStreamStatus::STREAMING)); |
| |
| // Start the configuration and stream Media content |
| ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type))); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); |
| } |
| |
| TEST_F(StateMachineTest, testStreamSkipEnablingSink) { |
| const auto context_type = kContextTypeRingtone; |
| const int leaudio_group_id = 4; |
| |
| // Prepare fake connected device group |
| auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type); |
| |
| /* Since we prepared device with Ringtone context in mind, only one ASE |
| * should have been configured. |
| */ |
| PrepareConfigureCodecHandler(group, 1); |
| PrepareConfigureQosHandler(group, 1); |
| PrepareEnableHandler(group, 1, false); |
| |
| auto* leAudioDevice = group->GetFirstDevice(); |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(3); |
| |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); |
| |
| InjectInitialIdleNotification(group); |
| |
| // Validate GroupStreamStatus |
| EXPECT_CALL( |
| mock_callbacks_, |
| StatusReportCb(leaudio_group_id, |
| bluetooth::le_audio::GroupStreamStatus::STREAMING)); |
| |
| // Start the configuration and stream Media content |
| ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type))); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); |
| } |
| |
| TEST_F(StateMachineTest, testStreamSkipEnablingSinkSource) { |
| const auto context_type = kContextTypeConversational; |
| const int leaudio_group_id = 4; |
| |
| // Prepare fake connected device group |
| auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type); |
| |
| /* Since we prepared device with Conversional context in mind, one Sink ASE |
| * and one Source ASE should have been configured. |
| */ |
| PrepareConfigureCodecHandler(group, 2); |
| PrepareConfigureQosHandler(group, 2); |
| PrepareEnableHandler(group, 2, false); |
| PrepareReceiverStartReady(group, 1); |
| |
| auto* leAudioDevice = group->GetFirstDevice(); |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(4); |
| |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); |
| |
| InjectInitialIdleNotification(group); |
| |
| // Validate GroupStreamStatus |
| EXPECT_CALL( |
| mock_callbacks_, |
| StatusReportCb(leaudio_group_id, |
| bluetooth::le_audio::GroupStreamStatus::STREAMING)); |
| |
| // Start the configuration and stream Media content |
| ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type))); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); |
| } |
| |
| TEST_F(StateMachineTest, testStreamMultipleConversational) { |
| const auto context_type = kContextTypeConversational; |
| const auto leaudio_group_id = 4; |
| const auto num_devices = 2; |
| |
| // Prepare multiple fake connected devices in a group |
| auto* group = |
| PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices); |
| ASSERT_EQ(group->Size(), num_devices); |
| |
| PrepareConfigureCodecHandler(group); |
| PrepareConfigureQosHandler(group); |
| PrepareEnableHandler(group); |
| PrepareReceiverStartReady(group, 1); |
| |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(AtLeast(1)); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(3); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); |
| |
| InjectInitialIdleNotification(group); |
| |
| auto* leAudioDevice = group->GetFirstDevice(); |
| auto expected_devices_written = 0; |
| while (leAudioDevice) { |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(leAudioDevice->conn_id_, |
| leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(AtLeast(3)); |
| expected_devices_written++; |
| leAudioDevice = group->GetNextDevice(leAudioDevice); |
| } |
| ASSERT_EQ(expected_devices_written, num_devices); |
| |
| // Validate GroupStreamStatus |
| EXPECT_CALL( |
| mock_callbacks_, |
| StatusReportCb(leaudio_group_id, |
| bluetooth::le_audio::GroupStreamStatus::STREAMING)); |
| |
| // Start the configuration and stream Media content |
| ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type))); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); |
| } |
| |
| TEST_F(StateMachineTest, testStreamMultiple) { |
| const auto context_type = kContextTypeMedia; |
| const auto leaudio_group_id = 4; |
| const auto num_devices = 2; |
| |
| // Prepare multiple fake connected devices in a group |
| auto* group = |
| PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices); |
| ASSERT_EQ(group->Size(), num_devices); |
| |
| PrepareConfigureCodecHandler(group); |
| PrepareConfigureQosHandler(group); |
| PrepareEnableHandler(group); |
| |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(AtLeast(1)); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); |
| |
| InjectInitialIdleNotification(group); |
| |
| auto* leAudioDevice = group->GetFirstDevice(); |
| auto expected_devices_written = 0; |
| while (leAudioDevice) { |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(leAudioDevice->conn_id_, |
| leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(AtLeast(3)); |
| expected_devices_written++; |
| leAudioDevice = group->GetNextDevice(leAudioDevice); |
| } |
| ASSERT_EQ(expected_devices_written, num_devices); |
| |
| // Validate GroupStreamStatus |
| EXPECT_CALL( |
| mock_callbacks_, |
| StatusReportCb(leaudio_group_id, |
| bluetooth::le_audio::GroupStreamStatus::STREAMING)); |
| |
| // Start the configuration and stream Media content |
| ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type))); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); |
| } |
| |
| TEST_F(StateMachineTest, testDisableSingle) { |
| const auto context_type = kContextTypeRingtone; |
| const int leaudio_group_id = 4; |
| |
| // Prepare fake connected device group |
| auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type); |
| |
| /* Since we prepared device with Ringtone context in mind, only one ASE |
| * should have been configured. |
| */ |
| PrepareConfigureCodecHandler(group, 1); |
| PrepareConfigureQosHandler(group, 1); |
| PrepareEnableHandler(group, 1); |
| PrepareDisableHandler(group, 1); |
| |
| auto* leAudioDevice = group->GetFirstDevice(); |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(4); |
| |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); |
| |
| InjectInitialIdleNotification(group); |
| |
| // Start the configuration and stream Media content |
| LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type)); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); |
| |
| // Validate GroupStreamStatus |
| EXPECT_CALL( |
| mock_callbacks_, |
| StatusReportCb(leaudio_group_id, |
| bluetooth::le_audio::GroupStreamStatus::SUSPENDED)); |
| |
| // Suspend the stream |
| LeAudioGroupStateMachine::Get()->SuspendStream(group); |
| |
| // Check if group has transition to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED); |
| } |
| |
| TEST_F(StateMachineTest, testDisableMultiple) { |
| const auto context_type = kContextTypeMedia; |
| const auto leaudio_group_id = 4; |
| const auto num_devices = 2; |
| |
| // Prepare multiple fake connected devices in a group |
| auto* group = |
| PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices); |
| ASSERT_EQ(group->Size(), num_devices); |
| |
| PrepareConfigureCodecHandler(group); |
| PrepareConfigureQosHandler(group); |
| PrepareEnableHandler(group); |
| PrepareDisableHandler(group); |
| |
| auto* leAudioDevice = group->GetFirstDevice(); |
| auto expected_devices_written = 0; |
| while (leAudioDevice) { |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(leAudioDevice->conn_id_, |
| leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(AtLeast(4)); |
| expected_devices_written++; |
| leAudioDevice = group->GetNextDevice(leAudioDevice); |
| } |
| ASSERT_EQ(expected_devices_written, num_devices); |
| |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(2); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(2); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); |
| |
| InjectInitialIdleNotification(group); |
| |
| // Start the configuration and stream Media content |
| LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type)); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); |
| |
| // Validate GroupStreamStatus |
| EXPECT_CALL( |
| mock_callbacks_, |
| StatusReportCb(leaudio_group_id, |
| bluetooth::le_audio::GroupStreamStatus::SUSPENDED)); |
| |
| // Suspend the stream |
| LeAudioGroupStateMachine::Get()->SuspendStream(group); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED); |
| } |
| |
| TEST_F(StateMachineTest, testDisableBidirectional) { |
| const auto context_type = kContextTypeConversational; |
| const int leaudio_group_id = 4; |
| |
| // Prepare fake connected device group |
| auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type); |
| |
| /* Since we prepared device with Conversional context in mind, Sink and Source |
| * ASEs should have been configured. |
| */ |
| PrepareConfigureCodecHandler(group, 2); |
| PrepareConfigureQosHandler(group, 2); |
| PrepareEnableHandler(group, 2); |
| PrepareDisableHandler(group, 2); |
| PrepareReceiverStartReady(group, 1); |
| PrepareReceiverStopReady(group, 1); |
| |
| auto* leAudioDevice = group->GetFirstDevice(); |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(AtLeast(4)); |
| |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); |
| |
| // Start the configuration and stream Media content |
| LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type)); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); |
| |
| // Suspend the stream |
| LeAudioGroupStateMachine::Get()->SuspendStream(group); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED); |
| } |
| |
| TEST_F(StateMachineTest, testReleaseSingle) { |
| const auto context_type = kContextTypeRingtone; |
| const int leaudio_group_id = 4; |
| |
| // Prepare fake connected device group |
| auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type); |
| |
| /* Since we prepared device with Ringtone context in mind, only one ASE |
| * should have been configured. |
| */ |
| PrepareConfigureCodecHandler(group, 1); |
| PrepareConfigureQosHandler(group, 1); |
| PrepareEnableHandler(group, 1); |
| PrepareDisableHandler(group, 1); |
| PrepareReleaseHandler(group, 1); |
| |
| auto* leAudioDevice = group->GetFirstDevice(); |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(4); |
| |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1); |
| |
| InjectInitialIdleNotification(group); |
| |
| // Start the configuration and stream Media content |
| LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type)); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); |
| |
| // Validate GroupStreamStatus |
| EXPECT_CALL(mock_callbacks_, |
| StatusReportCb(leaudio_group_id, |
| bluetooth::le_audio::GroupStreamStatus::IDLE)); |
| |
| // Stop the stream |
| LeAudioGroupStateMachine::Get()->StopStream(group); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE); |
| } |
| |
| TEST_F(StateMachineTest, testReleaseCachingSingle) { |
| const auto context_type = kContextTypeRingtone; |
| const int leaudio_group_id = 4; |
| |
| // Prepare fake connected device group |
| auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type); |
| |
| /* Since we prepared device with Ringtone context in mind, only one ASE |
| * should have been configured. |
| */ |
| PrepareConfigureCodecHandler(group, 1, true); |
| PrepareConfigureQosHandler(group, 1); |
| PrepareEnableHandler(group, 1); |
| PrepareDisableHandler(group, 1); |
| PrepareReleaseHandler(group, 1); |
| |
| auto* leAudioDevice = group->GetFirstDevice(); |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(4); |
| |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1); |
| |
| InjectInitialIdleNotification(group); |
| |
| // Validate GroupStreamStatus |
| EXPECT_CALL(mock_callbacks_, |
| StatusReportCb(leaudio_group_id, |
| bluetooth::le_audio::GroupStreamStatus::IDLE)); |
| EXPECT_CALL( |
| mock_callbacks_, |
| StatusReportCb(leaudio_group_id, |
| bluetooth::le_audio::GroupStreamStatus::STREAMING)); |
| |
| // Start the configuration and stream Media content |
| LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type)); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); |
| |
| // Stop the stream |
| LeAudioGroupStateMachine::Get()->StopStream(group); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED); |
| } |
| |
| TEST_F(StateMachineTest, testStreamCachingSingle) { |
| const auto context_type = kContextTypeRingtone; |
| const int leaudio_group_id = 4; |
| |
| // Prepare fake connected device group |
| auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type); |
| |
| /* Since we prepared device with Ringtone context in mind, only one ASE |
| * should have been configured. |
| */ |
| PrepareConfigureCodecHandler(group, 1, true); |
| PrepareConfigureQosHandler(group, 1); |
| PrepareEnableHandler(group, 1); |
| PrepareDisableHandler(group, 1); |
| PrepareReleaseHandler(group, 1); |
| |
| auto* leAudioDevice = group->GetFirstDevice(); |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(4 + 3); |
| |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(2); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(2); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1); |
| |
| InjectInitialIdleNotification(group); |
| |
| // Validate GroupStreamStatus |
| EXPECT_CALL(mock_callbacks_, |
| StatusReportCb(leaudio_group_id, |
| bluetooth::le_audio::GroupStreamStatus::IDLE)); |
| |
| EXPECT_CALL(mock_callbacks_, |
| StatusReportCb(leaudio_group_id, |
| bluetooth::le_audio::GroupStreamStatus::STREAMING)) |
| .Times(2); |
| |
| // Start the configuration and stream Media content |
| LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type)); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); |
| |
| // Stop the stream |
| LeAudioGroupStateMachine::Get()->StopStream(group); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED); |
| |
| // Start the configuration and stream Media content |
| LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type)); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); |
| } |
| |
| TEST_F(StateMachineTest, testReleaseMultiple) { |
| const auto context_type = kContextTypeMedia; |
| const auto leaudio_group_id = 6; |
| const auto num_devices = 2; |
| |
| // Prepare multiple fake connected devices in a group |
| auto* group = |
| PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices); |
| ASSERT_EQ(group->Size(), num_devices); |
| |
| PrepareConfigureCodecHandler(group); |
| PrepareConfigureQosHandler(group); |
| PrepareEnableHandler(group); |
| PrepareDisableHandler(group); |
| PrepareReleaseHandler(group); |
| |
| auto* leAudioDevice = group->GetFirstDevice(); |
| auto expected_devices_written = 0; |
| while (leAudioDevice) { |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(leAudioDevice->conn_id_, |
| leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(AtLeast(4)); |
| expected_devices_written++; |
| leAudioDevice = group->GetNextDevice(leAudioDevice); |
| } |
| ASSERT_EQ(expected_devices_written, num_devices); |
| |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(2); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(2); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1); |
| |
| InjectInitialIdleNotification(group); |
| |
| // Start the configuration and stream Media content |
| LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type)); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); |
| |
| // Validate GroupStreamStatus |
| EXPECT_CALL(mock_callbacks_, |
| StatusReportCb(leaudio_group_id, |
| bluetooth::le_audio::GroupStreamStatus::IDLE)); |
| |
| // Stop the stream |
| LeAudioGroupStateMachine::Get()->StopStream(group); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE); |
| } |
| |
| TEST_F(StateMachineTest, testReleaseBidirectional) { |
| const auto context_type = kContextTypeConversational; |
| const auto leaudio_group_id = 6; |
| |
| // Prepare fake connected device group |
| auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type); |
| |
| /* Since we prepared device with Conversional context in mind, Sink and Source |
| * ASEs should have been configured. |
| */ |
| PrepareConfigureCodecHandler(group, 2); |
| PrepareConfigureQosHandler(group, 2); |
| PrepareEnableHandler(group, 2); |
| PrepareDisableHandler(group, 2); |
| PrepareReceiverStartReady(group, 1); |
| PrepareReleaseHandler(group, 2); |
| |
| auto* leAudioDevice = group->GetFirstDevice(); |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(AtLeast(4)); |
| |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1); |
| |
| InjectInitialIdleNotification(group); |
| |
| // Start the configuration and stream Media content |
| LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type)); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), |
| types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); |
| |
| // Stop the stream |
| LeAudioGroupStateMachine::Get()->StopStream(group); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE); |
| } |
| |
| TEST_F(StateMachineTest, testDisableAndReleaseBidirectional) { |
| const auto context_type = kContextTypeConversational; |
| const int leaudio_group_id = 4; |
| |
| // Prepare fake connected device group |
| auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type); |
| |
| /* Since we prepared device with Conversional context in mind, Sink and Source |
| * ASEs should have been configured. |
| */ |
| PrepareConfigureCodecHandler(group, 2); |
| PrepareConfigureQosHandler(group, 2); |
| PrepareEnableHandler(group, 2); |
| PrepareDisableHandler(group, 2); |
| PrepareReceiverStartReady(group, 1); |
| PrepareReceiverStopReady(group, 1); |
| PrepareReleaseHandler(group, 2); |
| |
| auto* leAudioDevice = group->GetFirstDevice(); |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(AtLeast(4)); |
| |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(1); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(1); |
| |
| // Start the configuration and stream Media content |
| LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type)); |
| |
| // Suspend the stream |
| LeAudioGroupStateMachine::Get()->SuspendStream(group); |
| |
| // Stop the stream |
| LeAudioGroupStateMachine::Get()->StopStream(group); |
| |
| // Check if group has transitioned to a proper state |
| ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE); |
| } |
| |
| TEST_F(StateMachineTest, testAseIdAssignmentIdle) { |
| const auto context_type = kContextTypeConversational; |
| const auto leaudio_group_id = 6; |
| const auto num_devices = 1; |
| |
| // Prepare multiple fake connected devices in a group |
| auto* group = |
| PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices); |
| ASSERT_EQ(group->Size(), num_devices); |
| |
| // Should not trigger any action on our side |
| EXPECT_CALL(gatt_queue, WriteCharacteristic(_, _, _, _, _, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); |
| |
| for (auto* device = group->GetFirstDevice(); device != nullptr; |
| device = group->GetNextDevice(device)) { |
| for (auto& ase : device->ases_) { |
| ASSERT_EQ(ase.id, le_audio::types::ase::kAseIdInvalid); |
| InjectAseStateNotification(&ase, device, group, ascs::kAseStateIdle, |
| nullptr); |
| ASSERT_EQ(ase.id, ase_id_last_assigned); |
| } |
| } |
| } |
| |
| TEST_F(StateMachineTest, testAseIdAssignmentCodecConfigured) { |
| const auto context_type = kContextTypeConversational; |
| const auto leaudio_group_id = 6; |
| const auto num_devices = 1; |
| |
| // Prepare multiple fake connected devices in a group |
| auto* group = |
| PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices); |
| ASSERT_EQ(group->Size(), num_devices); |
| |
| // Should not trigger any action on our side |
| EXPECT_CALL(gatt_queue, WriteCharacteristic(_, _, _, _, _, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0); |
| EXPECT_CALL(*mock_iso_manager_, RemoveCig(_)).Times(0); |
| |
| for (auto* device = group->GetFirstDevice(); device != nullptr; |
| device = group->GetNextDevice(device)) { |
| for (auto& ase : device->ases_) { |
| client_parser::ascs::ase_codec_configured_state_params |
| codec_configured_state_params; |
| |
| ASSERT_EQ(ase.id, le_audio::types::ase::kAseIdInvalid); |
| InjectAseStateNotification(&ase, device, group, |
| ascs::kAseStateCodecConfigured, |
| &codec_configured_state_params); |
| ASSERT_EQ(ase.id, ase_id_last_assigned); |
| } |
| } |
| } |
| |
| TEST_F(StateMachineTest, testAseAutonomousRelease) { |
| const auto context_type = kContextTypeConversational; |
| const int leaudio_group_id = 4; |
| |
| // Prepare fake connected device group |
| auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type); |
| |
| /* Since we prepared device with Conversional context in mind, Sink and Source |
| * ASEs should have been configured. |
| */ |
| PrepareConfigureCodecHandler(group, 2); |
| PrepareConfigureQosHandler(group, 2); |
| PrepareEnableHandler(group, 2); |
| PrepareDisableHandler(group, 2); |
| PrepareReceiverStartReady(group, 1); |
| PrepareReceiverStopReady(group, 1); |
| PrepareReleaseHandler(group, 2); |
| |
| InjectInitialIdleNotification(group); |
| |
| // Validate initial GroupStreamStatus |
| EXPECT_CALL( |
| mock_callbacks_, |
| StatusReportCb(leaudio_group_id, |
| bluetooth::le_audio::GroupStreamStatus::STREAMING)); |
| |
| // Start the configuration and stream Media content |
| ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type))); |
| |
| // Validate new GroupStreamStatus |
| EXPECT_CALL(mock_callbacks_, |
| StatusReportCb(leaudio_group_id, |
| bluetooth::le_audio::GroupStreamStatus::IDLE)); |
| |
| for (auto* device = group->GetFirstDevice(); device != nullptr; |
| device = group->GetNextDevice(device)) { |
| for (auto& ase : device->ases_) { |
| client_parser::ascs::ase_codec_configured_state_params |
| codec_configured_state_params; |
| |
| ASSERT_EQ(ase.state, types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING); |
| |
| // Each one does the autonomous release |
| InjectAseStateNotification(&ase, device, group, ascs::kAseStateReleasing, |
| &codec_configured_state_params); |
| InjectAseStateNotification(&ase, device, group, ascs::kAseStateIdle, |
| &codec_configured_state_params); |
| } |
| } |
| |
| // Verify we've handled the release and updated all states |
| for (auto* device = group->GetFirstDevice(); device != nullptr; |
| device = group->GetNextDevice(device)) { |
| for (auto& ase : device->ases_) { |
| ASSERT_EQ(ase.state, types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE); |
| } |
| } |
| } |
| |
| TEST_F(StateMachineTest, testStateTransitionTimeout) { |
| const auto context_type = kContextTypeRingtone; |
| const int leaudio_group_id = 4; |
| |
| // Prepare fake connected device group |
| auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type); |
| |
| /* Since we prepared device with Ringtone context in mind, only one ASE |
| * should have been configured. |
| */ |
| PrepareConfigureCodecHandler(group, 1); |
| PrepareConfigureQosHandler(group, 1); |
| |
| auto* leAudioDevice = group->GetFirstDevice(); |
| EXPECT_CALL(gatt_queue, |
| WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _, |
| GATT_WRITE_NO_RSP, _, _)) |
| .Times(3); |
| |
| // Start the configuration and stream Media content |
| ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream( |
| group, static_cast<types::LeAudioContextType>(context_type))); |
| |
| // Check if timeout is fired |
| EXPECT_CALL(mock_callbacks_, OnStateTransitionTimeout(leaudio_group_id)); |
| |
| // simulate timeout seconds passed, alarm executing |
| fake_osi_alarm_set_on_mloop_.cb(fake_osi_alarm_set_on_mloop_.data); |
| ASSERT_EQ(1, mock_function_count_map["alarm_set_on_mloop"]); |
| } |
| } // namespace internal |
| } // namespace le_audio |