| /* |
| * Copyright 2021 HIMSA II K/S - www.himsa.com. |
| * Represented by EHIMA - www.ehima.com |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include <base/functional/bind.h> |
| #include <base/functional/callback.h> |
| #include <base/logging.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <hardware/bt_gatt_types.h> |
| #include <hardware/bt_has.h> |
| |
| #include <list> |
| #include <map> |
| #include <mutex> |
| #include <string> |
| #include <vector> |
| |
| #include "bta_csis_api.h" |
| #include "bta_gatt_api.h" |
| #include "bta_gatt_queue.h" |
| #include "bta_has_api.h" |
| #include "bta_le_audio_uuids.h" |
| #include "btm_sec.h" |
| #include "gap_api.h" |
| #include "gatt_api.h" |
| #include "has_types.h" |
| #include "include/check.h" |
| #include "internal_include/bt_trace.h" |
| #include "os/log.h" |
| #include "osi/include/properties.h" |
| #include "stack/include/bt_types.h" |
| |
| using base::Closure; |
| using bluetooth::Uuid; |
| using bluetooth::csis::CsisClient; |
| using bluetooth::has::ConnectionState; |
| using bluetooth::has::ErrorCode; |
| using bluetooth::has::kFeatureBitPresetSynchronizationSupported; |
| using bluetooth::has::kHasPresetIndexInvalid; |
| using bluetooth::has::PresetInfo; |
| using bluetooth::has::PresetInfoReason; |
| using le_audio::has::HasClient; |
| using le_audio::has::HasCtpGroupOpCoordinator; |
| using le_audio::has::HasCtpNtf; |
| using le_audio::has::HasCtpOp; |
| using le_audio::has::HasDevice; |
| using le_audio::has::HasGattOpContext; |
| using le_audio::has::HasJournalRecord; |
| using le_audio::has::HasPreset; |
| using le_audio::has::kControlPointMandatoryOpcodesBitmask; |
| using le_audio::has::kControlPointSynchronizedOpcodesBitmask; |
| using le_audio::has::kUuidActivePresetIndex; |
| using le_audio::has::kUuidHearingAccessService; |
| using le_audio::has::kUuidHearingAidFeatures; |
| using le_audio::has::kUuidHearingAidPresetControlPoint; |
| using le_audio::has::PresetCtpChangeId; |
| using le_audio::has::PresetCtpOpcode; |
| |
| void btif_storage_add_leaudio_has_device(const RawAddress& address, |
| std::vector<uint8_t> presets_bin, |
| uint8_t features, |
| uint8_t active_preset); |
| bool btif_storage_get_leaudio_has_presets(const RawAddress& address, |
| std::vector<uint8_t>& presets_bin, |
| uint8_t& active_preset); |
| void btif_storage_set_leaudio_has_presets(const RawAddress& address, |
| std::vector<uint8_t> presets_bin); |
| bool btif_storage_get_leaudio_has_features(const RawAddress& address, |
| uint8_t& features); |
| void btif_storage_set_leaudio_has_features(const RawAddress& address, |
| uint8_t features); |
| void btif_storage_set_leaudio_has_active_preset(const RawAddress& address, |
| uint8_t active_preset); |
| void btif_storage_remove_leaudio_has(const RawAddress& address); |
| |
| bool gatt_profile_get_eatt_support(const RawAddress& remote_bda); |
| |
| namespace { |
| class HasClientImpl; |
| HasClientImpl* instance; |
| std::mutex instance_mutex; |
| |
| /** |
| * ----------------------------------------------------------------------------- |
| * Hearing Access Service - Client role |
| * ----------------------------------------------------------------------------- |
| * Overview: |
| * |
| * This is Hearing Access Service client class. |
| * |
| * Each connected peer device supporting Hearing Access Service (HAS) is being |
| * connected and has its characteristics discovered. All the characteristics |
| * and descriptors (incl. the optional ones) are being read or written during |
| * this initial connection stage. Encryption is also verified. If all of this |
| * succeeds the appropriate callbacks are being called to notify upper layer |
| * about the successful HAS device connection and its features and the list |
| * of available audio configuration presets. |
| * |
| * Each HA device is expected to have the HAS service instantiated. It must |
| * contain Hearing Aid Features characteristic and optionally Presets Control |
| * Point and Active Preset Index characteristics, allowing the user to read |
| * preset details, switch currently active preset and possibly rename some of |
| * them. |
| * |
| * Hearing Aid Features characteristic informs the client about the type of |
| * Hearign Aids device (Monaural, Binaural or Banded), which operations are |
| * supported via the Preset Control Point characteristic, about dynamically |
| * changing list of available presets, writable presets and the support for |
| * synchronised preset change operations on the Binaural Hearing Aid devices. |
| */ |
| class HasClientImpl : public HasClient { |
| public: |
| HasClientImpl(bluetooth::has::HasClientCallbacks* callbacks, |
| base::Closure initCb) |
| : gatt_if_(0), callbacks_(callbacks) { |
| BTA_GATTC_AppRegister( |
| [](tBTA_GATTC_EVT event, tBTA_GATTC* p_data) { |
| if (instance && p_data) instance->GattcCallback(event, p_data); |
| }, |
| base::Bind( |
| [](base::Closure initCb, uint8_t client_id, uint8_t status) { |
| if (status != GATT_SUCCESS) { |
| LOG(ERROR) << "Can't start Hearing Aid Service client " |
| "profile - no gatt clients left!"; |
| return; |
| } |
| instance->gatt_if_ = client_id; |
| initCb.Run(); |
| }, |
| initCb), |
| true); |
| } |
| |
| ~HasClientImpl() override = default; |
| |
| void Connect(const RawAddress& address) override { |
| DLOG(INFO) << __func__ << ": " << ADDRESS_TO_LOGGABLE_STR(address); |
| |
| std::vector<RawAddress> addresses = {address}; |
| auto csis_api = CsisClient::Get(); |
| if (csis_api != nullptr) { |
| // Connect entire CAS set of devices |
| auto group_id = csis_api->GetGroupId( |
| address, bluetooth::Uuid::From16Bit(UUID_COMMON_AUDIO_SERVICE)); |
| addresses = csis_api->GetDeviceList(group_id); |
| } |
| |
| if (addresses.empty()) { |
| LOG(WARNING) << __func__ << ": " << ADDRESS_TO_LOGGABLE_STR(address) |
| << " is not part of any set"; |
| addresses = {address}; |
| } |
| |
| for (auto const& addr : addresses) { |
| auto device = std::find_if(devices_.begin(), devices_.end(), |
| HasDevice::MatchAddress(addr)); |
| if (device == devices_.end()) { |
| devices_.emplace_back(addr, true); |
| BTA_GATTC_Open(gatt_if_, addr, BTM_BLE_DIRECT_CONNECTION, false); |
| |
| } else { |
| device->is_connecting_actively = true; |
| if (!device->IsConnected()) |
| BTA_GATTC_Open(gatt_if_, addr, BTM_BLE_DIRECT_CONNECTION, false); |
| } |
| } |
| } |
| |
| void AddFromStorage(const RawAddress& address, uint8_t features, |
| uint16_t is_acceptlisted) { |
| DLOG(INFO) << __func__ << ": " << ADDRESS_TO_LOGGABLE_STR(address) |
| << ", features=" << loghex(features) |
| << ", isAcceptlisted=" << is_acceptlisted; |
| |
| /* Notify upper layer about the device */ |
| callbacks_->OnDeviceAvailable(address, features); |
| if (is_acceptlisted) { |
| auto device = std::find_if(devices_.begin(), devices_.end(), |
| HasDevice::MatchAddress(address)); |
| if (device == devices_.end()) |
| devices_.push_back(HasDevice(address, features)); |
| |
| /* Connect in background */ |
| BTA_GATTC_Open(gatt_if_, address, BTM_BLE_BKG_CONNECT_ALLOW_LIST, false); |
| } |
| } |
| |
| void Disconnect(const RawAddress& address) override { |
| DLOG(INFO) << __func__ << ": " << ADDRESS_TO_LOGGABLE_STR(address); |
| |
| std::vector<RawAddress> addresses = {address}; |
| auto csis_api = CsisClient::Get(); |
| if (csis_api != nullptr) { |
| // Disconnect entire CAS set of devices |
| auto group_id = csis_api->GetGroupId( |
| address, bluetooth::Uuid::From16Bit(UUID_COMMON_AUDIO_SERVICE)); |
| addresses = csis_api->GetDeviceList(group_id); |
| } |
| |
| if (addresses.empty()) { |
| LOG(WARNING) << __func__ << ": " << ADDRESS_TO_LOGGABLE_STR(address) |
| << " is not part of any set"; |
| addresses = {address}; |
| } |
| |
| for (auto const& addr : addresses) { |
| auto device = std::find_if(devices_.begin(), devices_.end(), |
| HasDevice::MatchAddress(addr)); |
| if (device == devices_.end()) { |
| LOG(WARNING) << "Device not connected to profile" |
| << ADDRESS_TO_LOGGABLE_STR(addr); |
| return; |
| } |
| |
| auto conn_id = device->conn_id; |
| auto is_connecting_actively = device->is_connecting_actively; |
| devices_.erase(device); |
| |
| if (conn_id != GATT_INVALID_CONN_ID) { |
| BTA_GATTC_Close(conn_id); |
| callbacks_->OnConnectionState(ConnectionState::DISCONNECTED, addr); |
| } else { |
| /* Removes active connection. */ |
| if (is_connecting_actively) { |
| BTA_GATTC_CancelOpen(gatt_if_, addr, true); |
| callbacks_->OnConnectionState(ConnectionState::DISCONNECTED, addr); |
| } |
| } |
| |
| /* Removes all registrations for connection. */ |
| BTA_GATTC_CancelOpen(0, addr, false); |
| } |
| } |
| |
| void UpdateJournalOpEntryStatus(HasDevice& device, HasGattOpContext context, |
| tGATT_STATUS status) { |
| /* Find journal entry by the context and update */ |
| auto journal_entry = std::find_if( |
| device.has_journal_.begin(), device.has_journal_.end(), |
| [&context](auto const& record) { |
| if (record.is_operation) { |
| return HasGattOpContext(record.op_context_handle) == context; |
| } |
| return false; |
| }); |
| |
| if (journal_entry == device.has_journal_.end()) { |
| LOG(WARNING) << "Journaling error or journal length limit was set to " |
| "low. Unable to log the operation outcome."; |
| return; |
| } |
| |
| if (journal_entry == device.has_journal_.end()) { |
| LOG(ERROR) << __func__ |
| << " Unable to find operation context in the journal!"; |
| return; |
| } |
| |
| journal_entry->op_status = status; |
| } |
| |
| std::optional<HasCtpOp> ExtractPendingCtpOp(uint16_t op_id) { |
| auto op_it = |
| std::find_if(pending_operations_.begin(), pending_operations_.end(), |
| [op_id](auto const& el) { return op_id == el.op_id; }); |
| |
| if (op_it != pending_operations_.end()) { |
| auto op = *op_it; |
| pending_operations_.erase(op_it); |
| |
| return op; |
| } |
| return std::nullopt; |
| } |
| |
| void EnqueueCtpOp(HasCtpOp op) { pending_operations_.push_back(op); } |
| |
| void OnHasActivePresetCycleStatus(uint16_t conn_id, tGATT_STATUS status, |
| void* user_data) { |
| DLOG(INFO) << __func__ << " status: " << +status; |
| |
| auto device = GetDevice(conn_id); |
| if (!device) { |
| LOG(WARNING) << "Device not connected to profile, conn_id=" << +conn_id; |
| return; |
| } |
| |
| /* Journal update */ |
| LOG_ASSERT(user_data != nullptr) << "Has operation context is missing!"; |
| auto context = HasGattOpContext(user_data); |
| UpdateJournalOpEntryStatus(*device, context, status); |
| |
| auto op_opt = ExtractPendingCtpOp(context.ctp_op_id); |
| if (status == GATT_SUCCESS) return; |
| |
| /* This could be one of the coordinated group preset change request */ |
| pending_group_operation_timeouts_.erase(context.ctp_op_id); |
| |
| /* Error handling */ |
| if (!op_opt.has_value()) { |
| LOG(ERROR) << __func__ << " Unknown operation error"; |
| return; |
| } |
| auto op = op_opt.value(); |
| callbacks_->OnActivePresetSelectError(op.addr_or_group, |
| GattStatus2SvcErrorCode(status)); |
| |
| if (status == GATT_DATABASE_OUT_OF_SYNC) { |
| LOG_INFO("Database out of sync for %s", |
| ADDRESS_TO_LOGGABLE_CSTR(device->addr)); |
| ClearDeviceInformationAndStartSearch(device); |
| } |
| } |
| |
| void OnHasPresetNameSetStatus(uint16_t conn_id, tGATT_STATUS status, |
| void* user_data) { |
| auto device = GetDevice(conn_id); |
| if (!device) { |
| LOG(WARNING) << "Device not connected to profile, conn_id=" << +conn_id; |
| return; |
| } |
| |
| LOG_ASSERT(user_data != nullptr) << "Has operation context is missing!"; |
| HasGattOpContext context(user_data); |
| |
| /* Journal update */ |
| UpdateJournalOpEntryStatus(*device, context, status); |
| |
| auto op_opt = ExtractPendingCtpOp(context.ctp_op_id); |
| if (status == GATT_SUCCESS) return; |
| |
| /* This could be one of the coordinated group preset change request */ |
| pending_group_operation_timeouts_.erase(context.ctp_op_id); |
| |
| /* Error handling */ |
| if (!op_opt.has_value()) { |
| LOG(ERROR) << __func__ << " Unknown operation error"; |
| return; |
| } |
| auto op = op_opt.value(); |
| callbacks_->OnSetPresetNameError(device->addr, op.index, |
| GattStatus2SvcErrorCode(status)); |
| if (status == GATT_DATABASE_OUT_OF_SYNC) { |
| LOG_INFO("Database out of sync for %s", |
| ADDRESS_TO_LOGGABLE_CSTR(device->addr)); |
| ClearDeviceInformationAndStartSearch(device); |
| } |
| } |
| |
| void OnHasPresetNameGetStatus(uint16_t conn_id, tGATT_STATUS status, |
| void* user_data) { |
| auto device = GetDevice(conn_id); |
| if (!device) { |
| LOG(WARNING) << "Device not connected to profile, conn_id=" << +conn_id; |
| return; |
| } |
| |
| LOG_ASSERT(user_data != nullptr) << "Has operation context is missing!"; |
| HasGattOpContext context(user_data); |
| |
| /* Journal update */ |
| UpdateJournalOpEntryStatus(*device, context, status); |
| |
| auto op_opt = ExtractPendingCtpOp(context.ctp_op_id); |
| if (status == GATT_SUCCESS) return; |
| |
| /* Error handling */ |
| if (!op_opt.has_value()) { |
| LOG(ERROR) << __func__ << " Unknown operation error"; |
| return; |
| } |
| auto op = op_opt.value(); |
| callbacks_->OnPresetInfoError(device->addr, op.index, |
| GattStatus2SvcErrorCode(status)); |
| |
| if (status == GATT_DATABASE_OUT_OF_SYNC) { |
| LOG_INFO("Database out of sync for %s", |
| ADDRESS_TO_LOGGABLE_CSTR(device->addr)); |
| ClearDeviceInformationAndStartSearch(device); |
| } else { |
| LOG_ERROR("Devices %s: Control point not usable. Disconnecting!", |
| ADDRESS_TO_LOGGABLE_CSTR(device->addr)); |
| BTA_GATTC_Close(device->conn_id); |
| } |
| } |
| |
| void OnHasPresetIndexOperation(uint16_t conn_id, tGATT_STATUS status, |
| void* user_data) { |
| DLOG(INFO) << __func__; |
| |
| auto device = GetDevice(conn_id); |
| if (!device) { |
| LOG(WARNING) << "Device not connected to profile, conn_id=" << +conn_id; |
| return; |
| } |
| |
| LOG_ASSERT(user_data != nullptr) << "Has operation context is missing!"; |
| HasGattOpContext context(user_data); |
| |
| /* Journal update */ |
| UpdateJournalOpEntryStatus(*device, context, status); |
| |
| auto op_opt = ExtractPendingCtpOp(context.ctp_op_id); |
| if (status == GATT_SUCCESS) return; |
| |
| /* This could be one of the coordinated group preset change request */ |
| pending_group_operation_timeouts_.erase(context.ctp_op_id); |
| |
| /* Error handling */ |
| if (!op_opt.has_value()) { |
| LOG(ERROR) << __func__ << " Unknown operation error"; |
| return; |
| } |
| |
| auto op = op_opt.value(); |
| if (op.opcode == PresetCtpOpcode::READ_PRESETS) { |
| callbacks_->OnPresetInfoError(device->addr, op.index, |
| GattStatus2SvcErrorCode(status)); |
| |
| } else { |
| callbacks_->OnActivePresetSelectError(op.addr_or_group, |
| GattStatus2SvcErrorCode(status)); |
| } |
| |
| if (status == GATT_DATABASE_OUT_OF_SYNC) { |
| LOG_INFO("Database out of sync for %s", |
| ADDRESS_TO_LOGGABLE_CSTR(device->addr)); |
| ClearDeviceInformationAndStartSearch(device); |
| } else { |
| LOG_ERROR("Devices %s: Control point not usable. Disconnecting!", |
| ADDRESS_TO_LOGGABLE_CSTR(device->addr)); |
| BTA_GATTC_Close(device->conn_id); |
| } |
| } |
| |
| void CpReadAllPresetsOperation(HasCtpOp operation) { |
| DLOG(INFO) << __func__ << " Operation: " << operation; |
| |
| if (std::holds_alternative<int>(operation.addr_or_group)) { |
| LOG(ERROR) << __func__ |
| << " Read all presets on the entire group not supported."; |
| callbacks_->OnPresetInfoError(operation.addr_or_group, operation.index, |
| ErrorCode::OPERATION_NOT_POSSIBLE); |
| return; |
| } |
| |
| auto device = std::find_if( |
| devices_.begin(), devices_.end(), |
| HasDevice::MatchAddress(std::get<RawAddress>(operation.addr_or_group))); |
| if (device == devices_.end()) { |
| LOG(WARNING) << __func__ << " Device not connected to profile addr: " |
| << ADDRESS_TO_LOGGABLE_STR(std::get<RawAddress>(operation.addr_or_group)); |
| callbacks_->OnPresetInfoError(device->addr, operation.index, |
| ErrorCode::OPERATION_NOT_POSSIBLE); |
| return; |
| } |
| |
| if (!device->SupportsPresets()) { |
| callbacks_->OnPresetInfoError(device->addr, operation.index, |
| ErrorCode::OPERATION_NOT_SUPPORTED); |
| } |
| |
| auto context = HasGattOpContext(operation); |
| |
| /* Journal update */ |
| device->has_journal_.Append(HasJournalRecord(operation, context)); |
| |
| /* Write to control point */ |
| EnqueueCtpOp(operation); |
| BtaGattQueue::WriteCharacteristic( |
| device->conn_id, device->cp_handle, operation.ToCharacteristicValue(), |
| GATT_WRITE, |
| [](uint16_t conn_id, tGATT_STATUS status, uint16_t handle, uint16_t len, |
| const uint8_t* value, void* user_data) { |
| if (instance) |
| instance->OnHasPresetNameGetStatus(conn_id, status, user_data); |
| }, |
| context); |
| } |
| |
| ErrorCode CpPresetIndexOperationWriteReq(HasDevice& device, |
| HasCtpOp& operation) { |
| DLOG(INFO) << __func__ << " Operation: " << operation; |
| |
| if (!device.IsConnected()) return ErrorCode::OPERATION_NOT_POSSIBLE; |
| |
| if (!device.SupportsPresets()) return ErrorCode::OPERATION_NOT_SUPPORTED; |
| |
| if (!device.SupportsOperation(operation.opcode)) |
| return operation.IsGroupRequest() |
| ? ErrorCode::GROUP_OPERATION_NOT_SUPPORTED |
| : ErrorCode::OPERATION_NOT_SUPPORTED; |
| |
| if (!device.IsValidPreset(operation.index)) |
| return ErrorCode::INVALID_PRESET_INDEX; |
| |
| auto context = HasGattOpContext(operation); |
| |
| /* Journal update */ |
| device.has_journal_.Append(HasJournalRecord(operation, context)); |
| |
| /* Write to control point */ |
| EnqueueCtpOp(operation); |
| BtaGattQueue::WriteCharacteristic( |
| device.conn_id, device.cp_handle, operation.ToCharacteristicValue(), |
| GATT_WRITE, |
| [](uint16_t conn_id, tGATT_STATUS status, uint16_t handle, uint16_t len, |
| const uint8_t* value, void* user_data) { |
| if (instance) |
| instance->OnHasPresetIndexOperation(conn_id, status, user_data); |
| }, |
| context); |
| |
| return ErrorCode::NO_ERROR; |
| } |
| |
| bool AreAllDevicesAvailable(const std::vector<RawAddress>& addresses) { |
| for (auto& addr : addresses) { |
| auto device = std::find_if(devices_.begin(), devices_.end(), |
| HasDevice::MatchAddress(addr)); |
| if (device == devices_.end() || !device->IsConnected()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| ErrorCode CpPresetOperationCaller( |
| HasCtpOp operation, |
| std::function<ErrorCode(HasDevice& device, HasCtpOp& operation)> |
| write_cb) { |
| DLOG(INFO) << __func__ << " Operation: " << operation; |
| auto status = ErrorCode::NO_ERROR; |
| |
| if (operation.IsGroupRequest()) { |
| auto csis_api = CsisClient::Get(); |
| if (csis_api == nullptr) { |
| /* No CSIS means no group operations */ |
| status = ErrorCode::GROUP_OPERATION_NOT_SUPPORTED; |
| |
| } else { |
| auto group_id = operation.GetGroupId(); |
| auto addresses = csis_api->GetDeviceList(group_id); |
| |
| /* Perform the operation only when all the devices are available */ |
| if (!AreAllDevicesAvailable(addresses)) { |
| addresses.clear(); |
| } |
| |
| if (addresses.empty()) { |
| status = ErrorCode::OPERATION_NOT_POSSIBLE; |
| |
| } else { |
| /* Make this a coordinated operation */ |
| pending_group_operation_timeouts_.emplace( |
| operation.op_id, HasCtpGroupOpCoordinator(addresses, operation)); |
| |
| if (operation.IsSyncedOperation()) { |
| status = ErrorCode::GROUP_OPERATION_NOT_SUPPORTED; |
| |
| /* Clear the error if we find device to forward the operation */ |
| bool was_sent = false; |
| for (auto& addr : addresses) { |
| auto device = std::find_if(devices_.begin(), devices_.end(), |
| HasDevice::MatchAddress(addr)); |
| if (device != devices_.end()) { |
| status = write_cb(*device, operation); |
| if (status == ErrorCode::NO_ERROR) { |
| was_sent = true; |
| break; |
| } |
| } |
| } |
| if (!was_sent) status = ErrorCode::OPERATION_NOT_POSSIBLE; |
| |
| } else { |
| status = ErrorCode::GROUP_OPERATION_NOT_SUPPORTED; |
| |
| for (auto& addr : addresses) { |
| auto device = std::find_if(devices_.begin(), devices_.end(), |
| HasDevice::MatchAddress(addr)); |
| if (device != devices_.end()) { |
| status = write_cb(*device, operation); |
| if (status != ErrorCode::NO_ERROR) break; |
| } |
| } |
| } |
| |
| /* Erase group op coordinator on error */ |
| if (status != ErrorCode::NO_ERROR) { |
| pending_group_operation_timeouts_.erase(operation.op_id); |
| } |
| } |
| } |
| |
| } else { |
| auto device = std::find_if(devices_.begin(), devices_.end(), |
| HasDevice::MatchAddress(std::get<RawAddress>( |
| operation.addr_or_group))); |
| status = ErrorCode::OPERATION_NOT_POSSIBLE; |
| if (device != devices_.end()) status = write_cb(*device, operation); |
| } |
| |
| return status; |
| } |
| |
| void CpPresetIndexOperation(HasCtpOp operation) { |
| LOG(INFO) << __func__ << " Operation: " << operation; |
| |
| auto status = CpPresetOperationCaller( |
| operation, [](HasDevice& device, HasCtpOp operation) -> ErrorCode { |
| if (instance) |
| return instance->CpPresetIndexOperationWriteReq(device, operation); |
| return ErrorCode::OPERATION_NOT_POSSIBLE; |
| }); |
| |
| if (status != ErrorCode::NO_ERROR) { |
| switch (operation.opcode) { |
| case PresetCtpOpcode::READ_PRESETS: |
| LOG_ASSERT( |
| std::holds_alternative<RawAddress>(operation.addr_or_group)) |
| << " Unsupported group operation!"; |
| |
| callbacks_->OnPresetInfoError( |
| std::get<RawAddress>(operation.addr_or_group), operation.index, |
| status); |
| break; |
| case PresetCtpOpcode::SET_ACTIVE_PRESET: |
| case PresetCtpOpcode::SET_ACTIVE_PRESET_SYNC: |
| callbacks_->OnActivePresetSelectError(operation.addr_or_group, |
| status); |
| break; |
| default: |
| break; |
| } |
| } |
| } |
| |
| ErrorCode CpPresetsCycleOperationWriteReq(HasDevice& device, |
| HasCtpOp& operation) { |
| DLOG(INFO) << __func__ << " addr: " << ADDRESS_TO_LOGGABLE_STR(device.addr) |
| << " operation: " << operation; |
| |
| if (!device.IsConnected()) return ErrorCode::OPERATION_NOT_POSSIBLE; |
| |
| if (!device.SupportsPresets()) return ErrorCode::OPERATION_NOT_SUPPORTED; |
| |
| if (!device.SupportsOperation(operation.opcode)) |
| return operation.IsGroupRequest() |
| ? ErrorCode::GROUP_OPERATION_NOT_SUPPORTED |
| : ErrorCode::OPERATION_NOT_SUPPORTED; |
| |
| auto context = HasGattOpContext(operation); |
| |
| /* Journal update */ |
| device.has_journal_.Append(HasJournalRecord(operation, context)); |
| |
| /* Write to control point */ |
| EnqueueCtpOp(operation); |
| BtaGattQueue::WriteCharacteristic( |
| device.conn_id, device.cp_handle, operation.ToCharacteristicValue(), |
| GATT_WRITE, |
| [](uint16_t conn_id, tGATT_STATUS status, uint16_t handle, uint16_t len, |
| const uint8_t* value, void* user_data) { |
| if (instance) |
| instance->OnHasActivePresetCycleStatus(conn_id, status, user_data); |
| }, |
| context); |
| return ErrorCode::NO_ERROR; |
| } |
| |
| void CpPresetsCycleOperation(HasCtpOp operation) { |
| DLOG(INFO) << __func__ << " Operation: " << operation; |
| |
| auto status = CpPresetOperationCaller( |
| operation, [](HasDevice& device, HasCtpOp operation) -> ErrorCode { |
| if (instance) |
| return instance->CpPresetsCycleOperationWriteReq(device, operation); |
| return ErrorCode::OPERATION_NOT_POSSIBLE; |
| }); |
| |
| if (status != ErrorCode::NO_ERROR) |
| callbacks_->OnActivePresetSelectError(operation.addr_or_group, status); |
| } |
| |
| ErrorCode CpWritePresetNameOperationWriteReq(HasDevice& device, |
| HasCtpOp operation) { |
| DLOG(INFO) << __func__ << " addr: " << ADDRESS_TO_LOGGABLE_STR(device.addr) |
| << " operation: " << operation; |
| |
| if (!device.IsConnected()) return ErrorCode::OPERATION_NOT_POSSIBLE; |
| |
| if (!device.SupportsPresets()) return ErrorCode::OPERATION_NOT_SUPPORTED; |
| |
| if (!device.IsValidPreset(operation.index, true)) |
| return device.IsValidPreset(operation.index) |
| ? ErrorCode::SET_NAME_NOT_ALLOWED |
| : ErrorCode::INVALID_PRESET_INDEX; |
| |
| if (!device.SupportsOperation(operation.opcode)) |
| return ErrorCode::OPERATION_NOT_SUPPORTED; |
| |
| if (operation.name.value_or("").length() > |
| le_audio::has::HasPreset::kPresetNameLengthLimit) |
| return ErrorCode::INVALID_PRESET_NAME_LENGTH; |
| |
| auto context = HasGattOpContext(operation, operation.index); |
| |
| /* Journal update */ |
| device.has_journal_.Append(HasJournalRecord(operation, context)); |
| |
| /* Write to control point */ |
| EnqueueCtpOp(operation); |
| BtaGattQueue::WriteCharacteristic( |
| device.conn_id, device.cp_handle, operation.ToCharacteristicValue(), |
| GATT_WRITE, |
| [](uint16_t conn_id, tGATT_STATUS status, uint16_t handle, uint16_t len, |
| const uint8_t* value, void* user_data) { |
| if (instance) |
| instance->OnHasPresetNameSetStatus(conn_id, status, user_data); |
| }, |
| context); |
| |
| return ErrorCode::NO_ERROR; |
| } |
| |
| void CpWritePresetNameOperation(HasCtpOp operation) { |
| DLOG(INFO) << __func__ << " operation: " << operation; |
| |
| auto status = ErrorCode::NO_ERROR; |
| |
| std::vector<RawAddress> addresses; |
| if (operation.IsGroupRequest()) { |
| auto csis_api = CsisClient::Get(); |
| if (csis_api != nullptr) { |
| addresses = csis_api->GetDeviceList(operation.GetGroupId()); |
| |
| /* Make this a coordinated operation */ |
| pending_group_operation_timeouts_.emplace( |
| operation.op_id, HasCtpGroupOpCoordinator(addresses, operation)); |
| } |
| |
| } else { |
| addresses = {operation.GetDeviceAddr()}; |
| } |
| |
| status = ErrorCode::OPERATION_NOT_POSSIBLE; |
| |
| /* Perform the operation only when all the devices are available */ |
| if (!AreAllDevicesAvailable(addresses)) { |
| addresses.clear(); |
| } |
| |
| for (auto& addr : addresses) { |
| auto device = std::find_if(devices_.begin(), devices_.end(), |
| HasDevice::MatchAddress(addr)); |
| if (device != devices_.end()) { |
| status = CpWritePresetNameOperationWriteReq(*device, operation); |
| if (status != ErrorCode::NO_ERROR) { |
| LOG(ERROR) << __func__ |
| << " Control point write error: " << (int)status; |
| break; |
| } |
| } |
| } |
| |
| if (status != ErrorCode::NO_ERROR) { |
| if (operation.IsGroupRequest()) |
| pending_group_operation_timeouts_.erase(operation.op_id); |
| |
| callbacks_->OnSetPresetNameError(operation.addr_or_group, operation.index, |
| status); |
| } |
| } |
| |
| bool shouldRequestSyncedOp(std::variant<RawAddress, int> addr_or_group_id, |
| PresetCtpOpcode opcode) { |
| /* Do not select locally synced ops when not performing group operations, |
| * You never know if the user will make another call for the other devices |
| * in this set even though the may support locally synced operations. |
| */ |
| if (std::holds_alternative<RawAddress>(addr_or_group_id)) return false; |
| |
| auto csis_api = CsisClient::Get(); |
| if (csis_api == nullptr) return false; |
| |
| auto addresses = csis_api->GetDeviceList(std::get<int>(addr_or_group_id)); |
| if (addresses.empty()) return false; |
| |
| for (auto& addr : addresses) { |
| auto device = std::find_if(devices_.begin(), devices_.end(), |
| HasDevice::MatchAddress(addr)); |
| if (device != devices_.end()) { |
| if (device->SupportsOperation(opcode)) return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| void SelectActivePreset(std::variant<RawAddress, int> addr_or_group_id, |
| uint8_t preset_index) override { |
| DLOG(INFO) << __func__; |
| |
| auto opcode = shouldRequestSyncedOp(addr_or_group_id, |
| PresetCtpOpcode::SET_ACTIVE_PRESET_SYNC) |
| ? PresetCtpOpcode::SET_ACTIVE_PRESET_SYNC |
| : PresetCtpOpcode::SET_ACTIVE_PRESET; |
| |
| CpPresetIndexOperation(HasCtpOp(addr_or_group_id, opcode, preset_index)); |
| } |
| |
| void NextActivePreset( |
| std::variant<RawAddress, int> addr_or_group_id) override { |
| DLOG(INFO) << __func__; |
| |
| auto opcode = shouldRequestSyncedOp(addr_or_group_id, |
| PresetCtpOpcode::SET_NEXT_PRESET_SYNC) |
| ? PresetCtpOpcode::SET_NEXT_PRESET_SYNC |
| : PresetCtpOpcode::SET_NEXT_PRESET; |
| |
| CpPresetsCycleOperation(HasCtpOp(addr_or_group_id, opcode)); |
| } |
| |
| void PreviousActivePreset( |
| std::variant<RawAddress, int> addr_or_group_id) override { |
| DLOG(INFO) << __func__; |
| |
| auto opcode = shouldRequestSyncedOp(addr_or_group_id, |
| PresetCtpOpcode::SET_PREV_PRESET_SYNC) |
| ? PresetCtpOpcode::SET_PREV_PRESET_SYNC |
| : PresetCtpOpcode::SET_PREV_PRESET; |
| |
| CpPresetsCycleOperation(HasCtpOp(addr_or_group_id, opcode)); |
| } |
| |
| void GetPresetInfo(const RawAddress& address, uint8_t preset_index) override { |
| auto device = std::find_if(devices_.begin(), devices_.end(), |
| HasDevice::MatchAddress(address)); |
| if (device == devices_.end()) { |
| LOG(WARNING) << "Device not connected to profile" |
| << ADDRESS_TO_LOGGABLE_STR(address); |
| return; |
| } |
| |
| DLOG(INFO) << __func__ << " preset idx: " << +preset_index; |
| |
| /* Due to mandatory control point notifications or indications, preset |
| * details are always up to date. However we have to be able to do the |
| * READ_PRESET_BY_INDEX, to pass the test specification requirements. |
| */ |
| if (osi_property_get_bool("persist.bluetooth.has.always_use_preset_cache", |
| true)) { |
| auto* preset = device->GetPreset(preset_index); |
| if (preset == nullptr) { |
| LOG(ERROR) << __func__ << "Invalid preset request" |
| << ADDRESS_TO_LOGGABLE_STR(address); |
| callbacks_->OnPresetInfoError(address, preset_index, |
| ErrorCode::INVALID_PRESET_INDEX); |
| return; |
| } |
| |
| callbacks_->OnPresetInfo(address, |
| PresetInfoReason::PRESET_INFO_REQUEST_RESPONSE, |
| {{.preset_index = preset_index, |
| .writable = preset->IsWritable(), |
| .available = preset->IsAvailable(), |
| .preset_name = preset->GetName()}}); |
| } else { |
| CpPresetIndexOperation( |
| HasCtpOp(address, PresetCtpOpcode::READ_PRESETS, preset_index)); |
| } |
| } |
| |
| void SetPresetName(std::variant<RawAddress, int> addr_or_group_id, |
| uint8_t preset_index, std::string name) override { |
| DLOG(INFO) << __func__ << "preset_idx: " << +preset_index |
| << ", name: " << name; |
| |
| CpWritePresetNameOperation(HasCtpOp(addr_or_group_id, |
| PresetCtpOpcode::WRITE_PRESET_NAME, |
| preset_index, 1 /* Don't care */, name)); |
| } |
| |
| void CleanUp() { |
| BTA_GATTC_AppDeregister(gatt_if_); |
| for (auto& device : devices_) { |
| if (device.conn_id != GATT_INVALID_CONN_ID) |
| BTA_GATTC_Close(device.conn_id); |
| DoDisconnectCleanUp(device); |
| } |
| |
| devices_.clear(); |
| pending_operations_.clear(); |
| } |
| |
| void Dump(int fd) const { |
| std::stringstream stream; |
| stream << " APP ID: " << +gatt_if_ << " \n"; |
| if (devices_.size()) { |
| stream << " {\"Known HAS devices\": ["; |
| for (const auto& device : devices_) { |
| stream << "\n {"; |
| device.Dump(stream); |
| stream << "\n },\n"; |
| } |
| stream << " ]}\n\n"; |
| } else { |
| stream << " \"No known HAS devices\"\n\n"; |
| } |
| dprintf(fd, "%s", stream.str().c_str()); |
| } |
| |
| void OnGroupOpCoordinatorTimeout(void* p) { |
| LOG(ERROR) << __func__ << ": Coordinated operation timeout: " |
| << " not all the devices notified their state change on time."; |
| |
| /* Clear pending group operations */ |
| pending_group_operation_timeouts_.clear(); |
| HasCtpGroupOpCoordinator::Cleanup(); |
| } |
| |
| private: |
| void WriteAllNeededCcc(const HasDevice& device) { |
| if (device.conn_id == GATT_INVALID_CONN_ID) { |
| LOG_ERROR("Device %s is not connected", |
| ADDRESS_TO_LOGGABLE_CSTR(device.addr)); |
| return; |
| } |
| |
| /* Write CCC values even remote should have it */ |
| LOG_INFO("Subscribing for notification/indications"); |
| if (device.SupportsFeaturesNotification()) { |
| SubscribeForNotifications(device.conn_id, device.addr, |
| device.features_handle, |
| device.features_ccc_handle); |
| } |
| |
| if (device.SupportsPresets()) { |
| SubscribeForNotifications(device.conn_id, device.addr, device.cp_handle, |
| device.cp_ccc_handle, device.cp_ccc_val); |
| SubscribeForNotifications(device.conn_id, device.addr, |
| device.active_preset_handle, |
| device.active_preset_ccc_handle); |
| } |
| |
| if (osi_property_get_bool("persist.bluetooth.has.always_use_preset_cache", |
| true) == false) { |
| CpReadAllPresetsOperation(HasCtpOp( |
| device.addr, PresetCtpOpcode::READ_PRESETS, |
| le_audio::has::kStartPresetIndex, le_audio::has::kMaxNumOfPresets)); |
| } |
| } |
| |
| void OnEncrypted(HasDevice& device) { |
| DLOG(INFO) << __func__ << ": " << ADDRESS_TO_LOGGABLE_STR(device.addr); |
| |
| if (device.isGattServiceValid()) { |
| device.is_connecting_actively = false; |
| NotifyHasDeviceValid(device); |
| callbacks_->OnPresetInfo(device.addr, PresetInfoReason::ALL_PRESET_INFO, |
| device.GetAllPresetInfo()); |
| callbacks_->OnActivePresetSelected(device.addr, |
| device.currently_active_preset); |
| WriteAllNeededCcc(device); |
| } else { |
| BTA_GATTC_ServiceSearchRequest(device.conn_id, |
| &kUuidHearingAccessService); |
| } |
| } |
| |
| void NotifyHasDeviceValid(const HasDevice& device) { |
| DLOG(INFO) << __func__ << " addr:" << ADDRESS_TO_LOGGABLE_STR(device.addr); |
| |
| std::vector<uint8_t> preset_indices; |
| preset_indices.reserve(device.has_presets.size()); |
| for (auto const& preset : device.has_presets) { |
| preset_indices.push_back(preset.GetIndex()); |
| } |
| |
| /* Notify that we are ready to go */ |
| callbacks_->OnConnectionState(ConnectionState::CONNECTED, device.addr); |
| } |
| |
| void MarkDeviceValidIfInInitialDiscovery(HasDevice& device) { |
| if (device.isGattServiceValid()) return; |
| |
| --device.gatt_svc_validation_steps; |
| |
| if (device.isGattServiceValid()) { |
| device.is_connecting_actively = false; |
| |
| std::vector<uint8_t> presets_bin; |
| if (device.SerializePresets(presets_bin)) { |
| btif_storage_add_leaudio_has_device(device.addr, presets_bin, |
| device.GetFeatures(), |
| device.currently_active_preset); |
| } |
| NotifyHasDeviceValid(device); |
| } |
| } |
| |
| void OnGattWriteCcc(uint16_t conn_id, tGATT_STATUS status, uint16_t handle, |
| void* user_data) { |
| DLOG(INFO) << __func__ << ": handle=" << loghex(handle); |
| |
| auto device = GetDevice(conn_id); |
| if (!device) { |
| LOG(ERROR) << __func__ << ": unknown conn_id=" << loghex(conn_id); |
| BtaGattQueue::Clean(conn_id); |
| return; |
| } |
| |
| if (status == GATT_DATABASE_OUT_OF_SYNC) { |
| LOG_INFO("Database out of sync for %s", |
| ADDRESS_TO_LOGGABLE_CSTR(device->addr)); |
| ClearDeviceInformationAndStartSearch(device); |
| return; |
| } |
| |
| HasGattOpContext context(user_data); |
| bool enabling_ntf = context.context_flags & |
| HasGattOpContext::kContextFlagsEnableNotification; |
| |
| if (handle == device->features_ccc_handle) { |
| if (status == GATT_SUCCESS) |
| device->features_notifications_enabled = enabling_ntf; |
| |
| } else if ((handle == device->active_preset_ccc_handle) || |
| (handle == device->cp_ccc_handle)) { |
| /* Both of these CCC are mandatory */ |
| if (enabling_ntf && (status != GATT_SUCCESS)) { |
| LOG(ERROR) << __func__ |
| << ": Failed to register for notifications on handle=" |
| << loghex(handle); |
| BTA_GATTC_Close(conn_id); |
| return; |
| } |
| } |
| } |
| |
| void OnHasNotification(uint16_t conn_id, uint16_t handle, uint16_t len, |
| const uint8_t* value) { |
| auto device = GetDevice(conn_id); |
| if (!device) { |
| LOG(WARNING) << "Skipping unknown device, conn_id=" << loghex(conn_id); |
| return; |
| } |
| |
| if (handle == device->features_handle) { |
| OnHasFeaturesValue(&(*device), GATT_SUCCESS, handle, len, value); |
| |
| } else if (handle == device->cp_handle) { |
| OnHasCtpValueNotification(&(*device), len, value); |
| |
| } else if (handle == device->active_preset_handle) { |
| OnHasActivePresetValue(&(*device), GATT_SUCCESS, handle, len, value); |
| } |
| } |
| |
| /* Gets the device from variant, possibly searching by conn_id */ |
| HasDevice* GetDevice( |
| std::variant<uint16_t, HasDevice*> conn_id_device_variant) { |
| HasDevice* device = nullptr; |
| |
| if (std::holds_alternative<HasDevice*>(conn_id_device_variant)) { |
| device = std::get<HasDevice*>(conn_id_device_variant); |
| } else { |
| auto it = std::find_if( |
| devices_.begin(), devices_.end(), |
| HasDevice::MatchConnId(std::get<uint16_t>(conn_id_device_variant))); |
| if (it != devices_.end()) device = &(*it); |
| } |
| |
| return device; |
| } |
| |
| void OnHasFeaturesValue( |
| std::variant<uint16_t, HasDevice*> conn_id_device_variant, |
| tGATT_STATUS status, uint16_t handle, uint16_t len, const uint8_t* value, |
| void* user_data = nullptr) { |
| DLOG(INFO) << __func__; |
| |
| auto device = GetDevice(conn_id_device_variant); |
| if (!device) { |
| LOG(ERROR) << __func__ << ": Unknown device!"; |
| return; |
| } |
| |
| if (status != GATT_SUCCESS) { |
| if (status == GATT_DATABASE_OUT_OF_SYNC) { |
| LOG_INFO("Database out of sync for %s", |
| ADDRESS_TO_LOGGABLE_CSTR(device->addr)); |
| ClearDeviceInformationAndStartSearch(device); |
| } else { |
| LOG_ERROR("Could not read characteristic at handle=0x%04x", handle); |
| BTA_GATTC_Close(device->conn_id); |
| } |
| return; |
| } |
| |
| if (len != 1) { |
| LOG(ERROR) << "Invalid features value length=" << +len |
| << " at handle=" << loghex(handle); |
| BTA_GATTC_Close(device->conn_id); |
| return; |
| } |
| |
| /* Store features value */ |
| uint8_t features; |
| STREAM_TO_UINT8(features, value); |
| device->UpdateFeatures(features); |
| |
| if (device->isGattServiceValid()) { |
| btif_storage_set_leaudio_has_features(device->addr, features); |
| } |
| |
| /* Journal update */ |
| device->has_journal_.Append(HasJournalRecord(features, true)); |
| |
| /* When service is not yet validated, report the available device with |
| * features. |
| */ |
| if (!device->isGattServiceValid()) |
| callbacks_->OnDeviceAvailable(device->addr, device->GetFeatures()); |
| |
| /* Notify features */ |
| callbacks_->OnFeaturesUpdate(device->addr, device->GetFeatures()); |
| |
| MarkDeviceValidIfInInitialDiscovery(*device); |
| } |
| |
| /* Translates GATT statuses to application specific error codes */ |
| static ErrorCode GattStatus2SvcErrorCode(tGATT_STATUS status) { |
| switch (status) { |
| case 0x80: |
| /* Invalid Opcode */ |
| /* Unlikely to happen as we would not allow unsupported operations */ |
| return ErrorCode::OPERATION_NOT_SUPPORTED; |
| case 0x81: |
| /* Write Name Not Allowed */ |
| return ErrorCode::SET_NAME_NOT_ALLOWED; |
| case 0x82: |
| /* Synchronization Not Supported */ |
| return ErrorCode::OPERATION_NOT_SUPPORTED; |
| case 0x83: |
| /* Preset Operation Not Possible */ |
| return ErrorCode::OPERATION_NOT_POSSIBLE; |
| case 0x84: |
| /* Preset Name Too Long */ |
| return ErrorCode::INVALID_PRESET_NAME_LENGTH; |
| case 0xFE: |
| /* Procedure Already in Progress */ |
| return ErrorCode::PROCEDURE_ALREADY_IN_PROGRESS; |
| default: |
| return ErrorCode::OPERATION_NOT_POSSIBLE; |
| } |
| } |
| |
| void OnHasPresetReadResponseNotification(HasDevice& device) { |
| DLOG(INFO) << __func__; |
| |
| while (device.ctp_notifications_.size() != 0) { |
| auto ntf = device.ctp_notifications_.front(); |
| /* Process only read response events */ |
| if (ntf.opcode != PresetCtpOpcode::READ_PRESET_RESPONSE) break; |
| |
| /* Update preset values */ |
| if (ntf.preset.has_value()) { |
| device.has_presets.erase(ntf.preset->GetIndex()); |
| device.has_presets.insert(ntf.preset.value()); |
| } |
| |
| /* We currently do READ_ALL_PRESETS only during the service validation. |
| * If service is already valid, this must be the READ_PRESET_BY_INDEX. |
| */ |
| if (device.isGattServiceValid()) { |
| auto info = device.GetPresetInfo(ntf.preset.value().GetIndex()); |
| if (info.has_value()) |
| callbacks_->OnPresetInfo( |
| device.addr, PresetInfoReason::PRESET_INFO_REQUEST_RESPONSE, |
| {{info.value()}}); |
| } |
| |
| /* Journal update */ |
| device.has_journal_.Append(HasJournalRecord(ntf)); |
| device.ctp_notifications_.pop_front(); |
| } |
| |
| auto in_svc_validation = !device.isGattServiceValid(); |
| MarkDeviceValidIfInInitialDiscovery(device); |
| |
| /* We currently do READ_ALL_PRESETS only during the service validation. |
| * ALL_PRESET_INFO will be sent only during this initial phase. |
| */ |
| if (in_svc_validation) { |
| callbacks_->OnPresetInfo(device.addr, PresetInfoReason::ALL_PRESET_INFO, |
| device.GetAllPresetInfo()); |
| |
| /* If this was the last validation step then send the currently active |
| * preset as well. |
| */ |
| if (device.isGattServiceValid()) |
| callbacks_->OnActivePresetSelected(device.addr, |
| device.currently_active_preset); |
| } |
| } |
| |
| void OnHasPresetGenericUpdate(HasDevice& device) { |
| DLOG(ERROR) << __func__; |
| |
| std::vector<PresetInfo> updated_infos; |
| std::vector<PresetInfo> deleted_infos; |
| |
| /* Process the entire train of preset changes with generic updates */ |
| while (device.ctp_notifications_.size() != 0) { |
| auto nt = device.ctp_notifications_.front(); |
| |
| /* Break if not a generic update anymore */ |
| if (nt.opcode != PresetCtpOpcode::PRESET_CHANGED) break; |
| if (nt.change_id != PresetCtpChangeId::PRESET_GENERIC_UPDATE) break; |
| |
| if (nt.preset.has_value()) { |
| /* Erase old value if exist */ |
| device.has_presets.erase(nt.preset->GetIndex()); |
| |
| /* Erase in-between indices */ |
| if (nt.prev_index != 0) { |
| auto it = device.has_presets.begin(); |
| while (it != device.has_presets.end()) { |
| if ((it->GetIndex() > nt.prev_index) && |
| (it->GetIndex() < nt.preset->GetIndex())) { |
| auto info = device.GetPresetInfo(it->GetIndex()); |
| if (info.has_value()) deleted_infos.push_back(info.value()); |
| |
| it = device.has_presets.erase(it); |
| |
| } else { |
| ++it; |
| } |
| } |
| } |
| /* Update presets */ |
| device.has_presets.insert(*nt.preset); |
| |
| auto info = device.GetPresetInfo(nt.preset->GetIndex()); |
| if (info.has_value()) updated_infos.push_back(info.value()); |
| } |
| |
| /* Journal update */ |
| device.has_journal_.Append(HasJournalRecord(nt)); |
| device.ctp_notifications_.pop_front(); |
| } |
| |
| if (device.isGattServiceValid()) { |
| /* Update preset values in the storage */ |
| std::vector<uint8_t> presets_bin; |
| if (device.SerializePresets(presets_bin)) { |
| btif_storage_set_leaudio_has_presets(device.addr, presets_bin); |
| } |
| |
| /* Check for the matching coordinated group op. to use group callbacks */ |
| for (auto it = pending_group_operation_timeouts_.rbegin(); |
| it != pending_group_operation_timeouts_.rend(); ++it) { |
| auto& group_op_coordinator = it->second; |
| |
| /* Here we interested only in valid preset name changes */ |
| if (!((group_op_coordinator.operation.opcode == |
| PresetCtpOpcode::WRITE_PRESET_NAME) && |
| group_op_coordinator.operation.name.has_value())) |
| continue; |
| |
| /* Match preset update results with the triggering operation */ |
| auto renamed_preset_info = std::find_if( |
| updated_infos.begin(), updated_infos.end(), |
| [&group_op_coordinator](const auto& info) { |
| return (group_op_coordinator.operation.name.value() == |
| info.preset_name); |
| }); |
| if (renamed_preset_info == updated_infos.end()) continue; |
| |
| if (group_op_coordinator.SetCompleted(device.addr)) { |
| group_op_coordinator.preset_info_verification_list.push_back( |
| *renamed_preset_info); |
| |
| /* Call the proper group operation completion callback */ |
| if (group_op_coordinator.IsFullyCompleted()) { |
| callbacks_->OnPresetInfo( |
| group_op_coordinator.operation.GetGroupId(), |
| PresetInfoReason::PRESET_INFO_UPDATE, {*renamed_preset_info}); |
| pending_group_operation_timeouts_.erase(it->first); |
| } |
| |
| /* Erase it from the 'updated_infos' since later we'll be sending |
| * this as a group callback when the other device completes the |
| * coordinated group name change. |
| * |
| * WARNING: There might an issue with callbacks call reordering due to |
| * some of them being kept for group callbacks called later, when all |
| * the grouped devices complete the coordinated group rename |
| * operation. In most cases this should not be a major problem. |
| */ |
| updated_infos.erase(renamed_preset_info); |
| break; |
| } |
| } |
| |
| if (!updated_infos.empty()) |
| callbacks_->OnPresetInfo( |
| device.addr, PresetInfoReason::PRESET_INFO_UPDATE, updated_infos); |
| |
| if (!deleted_infos.empty()) |
| callbacks_->OnPresetInfo(device.addr, PresetInfoReason::PRESET_DELETED, |
| deleted_infos); |
| } |
| } |
| |
| void OnHasPresetAvailabilityChanged(HasDevice& device) { |
| DLOG(INFO) << __func__; |
| |
| std::vector<PresetInfo> infos; |
| |
| while (device.ctp_notifications_.size() != 0) { |
| auto nt = device.ctp_notifications_.front(); |
| |
| /* Process only preset change notifications */ |
| if (nt.opcode != PresetCtpOpcode::PRESET_CHANGED) break; |
| |
| auto preset = device.has_presets.extract(nt.index).value(); |
| auto new_props = preset.GetProperties(); |
| |
| /* Process only the preset availability changes and then notify */ |
| if ((nt.change_id != PresetCtpChangeId::PRESET_AVAILABLE) && |
| (nt.change_id != PresetCtpChangeId::PRESET_UNAVAILABLE)) |
| break; |
| |
| /* Availability change */ |
| if (nt.change_id == PresetCtpChangeId::PRESET_AVAILABLE) { |
| new_props |= HasPreset::kPropertyAvailable; |
| } else { |
| new_props &= !HasPreset::kPropertyAvailable; |
| } |
| device.has_presets.insert( |
| HasPreset(preset.GetIndex(), new_props, preset.GetName())); |
| |
| auto info = device.GetPresetInfo(nt.index); |
| if (info.has_value()) infos.push_back(info.value()); |
| |
| /* Journal update */ |
| device.has_journal_.Append(HasJournalRecord(nt)); |
| device.ctp_notifications_.pop_front(); |
| } |
| |
| /* Update preset storage */ |
| if (device.isGattServiceValid()) { |
| std::vector<uint8_t> presets_bin; |
| if (device.SerializePresets(presets_bin)) { |
| btif_storage_set_leaudio_has_presets(device.addr, presets_bin); |
| } |
| } |
| |
| callbacks_->OnPresetInfo( |
| device.addr, PresetInfoReason::PRESET_AVAILABILITY_CHANGED, infos); |
| } |
| |
| void OnHasPresetDeleted(HasDevice& device) { |
| DLOG(INFO) << __func__; |
| |
| std::vector<PresetInfo> infos; |
| bool is_deleted = false; |
| |
| while (device.ctp_notifications_.size() != 0) { |
| auto nt = device.ctp_notifications_.front(); |
| |
| /* Process only preset change notifications */ |
| if (nt.opcode != PresetCtpOpcode::PRESET_CHANGED) break; |
| |
| /* Process only the deletions and then notify */ |
| if (nt.change_id != PresetCtpChangeId::PRESET_DELETED) break; |
| |
| auto info = device.GetPresetInfo(nt.index); |
| if (info.has_value()) infos.push_back(info.value()); |
| |
| if (device.has_presets.count(nt.index)) { |
| is_deleted = true; |
| device.has_presets.erase(nt.index); |
| } |
| |
| /* Journal update */ |
| device.has_journal_.Append(HasJournalRecord(nt)); |
| device.ctp_notifications_.pop_front(); |
| } |
| |
| /* Update preset storage */ |
| if (device.isGattServiceValid()) { |
| std::vector<uint8_t> presets_bin; |
| if (device.SerializePresets(presets_bin)) { |
| btif_storage_set_leaudio_has_presets(device.addr, presets_bin); |
| } |
| } |
| |
| if (is_deleted) |
| callbacks_->OnPresetInfo(device.addr, PresetInfoReason::PRESET_DELETED, |
| infos); |
| } |
| |
| void ProcessCtpNotificationQueue(HasDevice& device) { |
| std::vector<PresetInfo> infos; |
| |
| while (device.ctp_notifications_.size() != 0) { |
| auto ntf = device.ctp_notifications_.front(); |
| DLOG(INFO) << __func__ << " ntf: " << ntf; |
| |
| if (ntf.opcode == PresetCtpOpcode::PRESET_CHANGED) { |
| switch (ntf.change_id) { |
| case PresetCtpChangeId::PRESET_GENERIC_UPDATE: |
| OnHasPresetGenericUpdate(device); |
| break; |
| case PresetCtpChangeId::PRESET_AVAILABLE: |
| OnHasPresetAvailabilityChanged(device); |
| break; |
| case PresetCtpChangeId::PRESET_UNAVAILABLE: |
| OnHasPresetAvailabilityChanged(device); |
| break; |
| case PresetCtpChangeId::PRESET_DELETED: |
| OnHasPresetDeleted(device); |
| break; |
| default: |
| LOG(ERROR) << __func__ << " Invalid notification: " << ntf; |
| break; |
| } |
| |
| } else if (ntf.opcode == PresetCtpOpcode::READ_PRESET_RESPONSE) { |
| OnHasPresetReadResponseNotification(device); |
| |
| } else { |
| LOG(ERROR) << __func__ << " Unsupported preset notification: " << ntf; |
| } |
| } |
| } |
| |
| void OnHasCtpValueNotification(HasDevice* device, uint16_t len, |
| const uint8_t* value) { |
| auto ntf_opt = HasCtpNtf::FromCharacteristicValue(len, value); |
| if (!ntf_opt.has_value()) { |
| LOG(ERROR) << __func__ |
| << " Unhandled notification for device: " << *device; |
| BTA_GATTC_Close(device->conn_id); |
| return; |
| } |
| |
| auto ntf = ntf_opt.value(); |
| DLOG(INFO) << __func__ << ntf; |
| |
| device->ctp_notifications_.push_back(ntf); |
| if (ntf.is_last) ProcessCtpNotificationQueue(*device); |
| } |
| |
| void OnHasActivePresetValue( |
| std::variant<uint16_t, HasDevice*> conn_id_device_variant, |
| tGATT_STATUS status, uint16_t handle, uint16_t len, const uint8_t* value, |
| void* user_data = nullptr) { |
| DLOG(INFO) << __func__; |
| |
| auto device = GetDevice(conn_id_device_variant); |
| if (!device) { |
| LOG(ERROR) << "Skipping unknown device!"; |
| return; |
| } |
| |
| if (status != GATT_SUCCESS) { |
| if (status == GATT_DATABASE_OUT_OF_SYNC) { |
| LOG_INFO("Database out of sync for %s", |
| ADDRESS_TO_LOGGABLE_CSTR(device->addr)); |
| ClearDeviceInformationAndStartSearch(device); |
| } else { |
| LOG_ERROR("Could not read characteristic at handle=0x%04x", handle); |
| BTA_GATTC_Close(device->conn_id); |
| } |
| } |
| |
| if (len != 1) { |
| LOG(ERROR) << "Invalid preset value length=" << +len |
| << " at handle=" << loghex(handle); |
| BTA_GATTC_Close(device->conn_id); |
| return; |
| } |
| |
| /* Get the active preset value */ |
| auto* pp = value; |
| STREAM_TO_UINT8(device->currently_active_preset, pp); |
| |
| if (device->isGattServiceValid()) { |
| btif_storage_set_leaudio_has_active_preset( |
| device->addr, device->currently_active_preset); |
| } |
| |
| /* Journal update */ |
| device->has_journal_.Append( |
| HasJournalRecord(device->currently_active_preset, false)); |
| |
| /* If svc not marked valid, this might be the last validation step. */ |
| MarkDeviceValidIfInInitialDiscovery(*device); |
| |
| if (device->isGattServiceValid()) { |
| if (!pending_group_operation_timeouts_.empty()) { |
| for (auto it = pending_group_operation_timeouts_.rbegin(); |
| it != pending_group_operation_timeouts_.rend(); ++it) { |
| auto& group_op_coordinator = it->second; |
| |
| bool matches = false; |
| switch (group_op_coordinator.operation.opcode) { |
| case PresetCtpOpcode::SET_ACTIVE_PRESET: |
| [[fallthrough]]; |
| case PresetCtpOpcode::SET_NEXT_PRESET: |
| [[fallthrough]]; |
| case PresetCtpOpcode::SET_PREV_PRESET: |
| [[fallthrough]]; |
| case PresetCtpOpcode::SET_ACTIVE_PRESET_SYNC: |
| [[fallthrough]]; |
| case PresetCtpOpcode::SET_NEXT_PRESET_SYNC: |
| [[fallthrough]]; |
| case PresetCtpOpcode::SET_PREV_PRESET_SYNC: { |
| if (group_op_coordinator.SetCompleted(device->addr)) { |
| matches = true; |
| break; |
| } |
| } break; |
| default: |
| /* Ignore */ |
| break; |
| } |
| if (group_op_coordinator.IsFullyCompleted()) { |
| callbacks_->OnActivePresetSelected( |
| group_op_coordinator.operation.GetGroupId(), |
| device->currently_active_preset); |
| pending_group_operation_timeouts_.erase(it->first); |
| } |
| if (matches) break; |
| } |
| |
| } else { |
| callbacks_->OnActivePresetSelected(device->addr, |
| device->currently_active_preset); |
| } |
| } |
| } |
| |
| void DeregisterNotifications(HasDevice& device) { |
| /* Deregister from optional features notifications */ |
| if (device.features_ccc_handle != GAP_INVALID_HANDLE) { |
| BTA_GATTC_DeregisterForNotifications(gatt_if_, device.addr, |
| device.features_handle); |
| } |
| |
| /* Deregister from active presets notifications if presets exist */ |
| if (device.active_preset_ccc_handle != GAP_INVALID_HANDLE) { |
| BTA_GATTC_DeregisterForNotifications(gatt_if_, device.addr, |
| device.active_preset_handle); |
| } |
| |
| /* Deregister from control point notifications */ |
| if (device.cp_ccc_handle != GAP_INVALID_HANDLE) { |
| BTA_GATTC_DeregisterForNotifications(gatt_if_, device.addr, |
| device.cp_handle); |
| } |
| } |
| |
| /* Cleans up after the device disconnection */ |
| void DoDisconnectCleanUp(HasDevice& device, |
| bool invalidate_gatt_service = true) { |
| LOG_DEBUG(": device=%s", ADDRESS_TO_LOGGABLE_CSTR(device.addr)); |
| |
| DeregisterNotifications(device); |
| |
| if (device.conn_id != GATT_INVALID_CONN_ID) { |
| BtaGattQueue::Clean(device.conn_id); |
| if (invalidate_gatt_service) device.gatt_svc_validation_steps = 0xFE; |
| } |
| |
| /* Clear pending operations */ |
| auto addr = device.addr; |
| pending_operations_.erase( |
| std::remove_if( |
| pending_operations_.begin(), pending_operations_.end(), |
| [&addr](auto& el) { |
| if (std::holds_alternative<RawAddress>(el.addr_or_group)) { |
| return std::get<RawAddress>(el.addr_or_group) == addr; |
| } |
| return false; |
| }), |
| pending_operations_.end()); |
| |
| device.ConnectionCleanUp(); |
| } |
| |
| /* These below are all GATT service discovery, validation, cache & storage */ |
| bool CacheAttributeHandles(const gatt::Service& service, HasDevice* device) { |
| DLOG(INFO) << __func__ << ": device=" |
| << ADDRESS_TO_LOGGABLE_STR(device->addr); |
| |
| for (const gatt::Characteristic& charac : service.characteristics) { |
| if (charac.uuid == kUuidActivePresetIndex) { |
| /* Find the mandatory CCC descriptor */ |
| uint16_t ccc_handle = |
| FindCccHandle(device->conn_id, charac.value_handle); |
| if (ccc_handle == GAP_INVALID_HANDLE) { |
| LOG(ERROR) << __func__ |
| << ": no HAS Active Preset CCC descriptor found!"; |
| return false; |
| } |
| device->active_preset_ccc_handle = ccc_handle; |
| device->active_preset_handle = charac.value_handle; |
| |
| } else if (charac.uuid == kUuidHearingAidPresetControlPoint) { |
| /* Find the mandatory CCC descriptor */ |
| uint16_t ccc_handle = |
| FindCccHandle(device->conn_id, charac.value_handle); |
| if (ccc_handle == GAP_INVALID_HANDLE) { |
| LOG(ERROR) << __func__ |
| << ": no HAS Control Point CCC descriptor found!"; |
| return false; |
| } |
| uint8_t ccc_val = 0; |
| if (charac.properties & GATT_CHAR_PROP_BIT_NOTIFY) |
| ccc_val |= GATT_CHAR_CLIENT_CONFIG_NOTIFICATION; |
| |
| if (charac.properties & GATT_CHAR_PROP_BIT_INDICATE) |
| ccc_val |= GATT_CHAR_CLIENT_CONFIG_INDICTION; |
| |
| if (ccc_val == 0) { |
| LOG_ERROR("Invalid properties for the control point 0x%02x", |
| charac.properties); |
| return false; |
| } |
| |
| device->cp_ccc_handle = ccc_handle; |
| device->cp_handle = charac.value_handle; |
| device->cp_ccc_val = ccc_val; |
| } else if (charac.uuid == kUuidHearingAidFeatures) { |
| /* Find the optional CCC descriptor */ |
| uint16_t ccc_handle = |
| FindCccHandle(device->conn_id, charac.value_handle); |
| device->features_ccc_handle = ccc_handle; |
| device->features_handle = charac.value_handle; |
| } |
| } |
| return true; |
| } |
| |
| bool LoadHasDetailsFromStorage(HasDevice* device) { |
| DLOG(INFO) << __func__ << ": device=" |
| << ADDRESS_TO_LOGGABLE_STR(device->addr); |
| |
| std::vector<uint8_t> presets_bin; |
| uint8_t active_preset; |
| |
| if (!btif_storage_get_leaudio_has_presets(device->addr, presets_bin, |
| active_preset)) |
| return false; |
| |
| if (!HasDevice::DeserializePresets(presets_bin.data(), presets_bin.size(), |
| *device)) |
| return false; |
| |
| VLOG(1) << "Loading HAS service details from storage."; |
| |
| device->currently_active_preset = active_preset; |
| |
| /* Update features and refresh opcode support map */ |
| uint8_t val; |
| if (btif_storage_get_leaudio_has_features(device->addr, val)) |
| device->UpdateFeatures(val); |
| |
| /* With all the details loaded we can already mark it as valid */ |
| device->gatt_svc_validation_steps = 0; |
| device->is_connecting_actively = false; |
| |
| NotifyHasDeviceValid(*device); |
| callbacks_->OnPresetInfo(device->addr, PresetInfoReason::ALL_PRESET_INFO, |
| device->GetAllPresetInfo()); |
| callbacks_->OnActivePresetSelected(device->addr, |
| device->currently_active_preset); |
| if (device->conn_id == GATT_INVALID_CONN_ID) return true; |
| |
| /* Be mistrustful here: write CCC values even remote should have it */ |
| LOG_INFO("Subscribing for notification/indications"); |
| WriteAllNeededCcc(*device); |
| |
| return true; |
| } |
| |
| bool StartInitialHasDetailsReadAndValidation(const gatt::Service& service, |
| HasDevice* device) { |
| // Validate service structure |
| if (device->features_handle == GAP_INVALID_HANDLE) { |
| /* Missing key characteristic */ |
| LOG(ERROR) << __func__ << ": Service has broken structure"; |
| return false; |
| } |
| |
| if (device->cp_handle != GAP_INVALID_HANDLE) { |
| if (device->active_preset_handle == GAP_INVALID_HANDLE) return false; |
| if (device->active_preset_ccc_handle == GAP_INVALID_HANDLE) return false; |
| } |
| |
| /* Number of reads or notifications required to validate the service */ |
| device->gatt_svc_validation_steps = 1 + (device->SupportsPresets() ? 2 : 0); |
| |
| /* Read the initial features */ |
| BtaGattQueue::ReadCharacteristic( |
| device->conn_id, device->features_handle, |
| [](uint16_t conn_id, tGATT_STATUS status, uint16_t handle, uint16_t len, |
| uint8_t* value, void* user_data) { |
| if (instance) |
| instance->OnHasFeaturesValue(conn_id, status, handle, len, value, |
| user_data); |
| }, |
| nullptr); |
| |
| /* Register for features notifications */ |
| if (device->SupportsFeaturesNotification()) { |
| SubscribeForNotifications(device->conn_id, device->addr, |
| device->features_handle, |
| device->features_ccc_handle); |
| } else { |
| LOG(WARNING) << __func__ |
| << ": server does not support features notification"; |
| } |
| |
| /* If Presets are supported we should read them all and subscribe for the |
| * mandatory active preset index notifications. |
| */ |
| if (device->SupportsPresets()) { |
| /* Subscribe for active preset notifications */ |
| SubscribeForNotifications(device->conn_id, device->addr, |
| device->active_preset_handle, |
| device->active_preset_ccc_handle); |
| |
| SubscribeForNotifications(device->conn_id, device->addr, |
| device->cp_handle, device->cp_ccc_handle, |
| device->cp_ccc_val); |
| |
| /* Get all the presets */ |
| CpReadAllPresetsOperation(HasCtpOp( |
| device->addr, PresetCtpOpcode::READ_PRESETS, |
| le_audio::has::kStartPresetIndex, le_audio::has::kMaxNumOfPresets)); |
| |
| /* Read the current active preset index */ |
| BtaGattQueue::ReadCharacteristic( |
| device->conn_id, device->active_preset_handle, |
| [](uint16_t conn_id, tGATT_STATUS status, uint16_t handle, |
| uint16_t len, uint8_t* value, void* user_data) { |
| if (instance) |
| instance->OnHasActivePresetValue(conn_id, status, handle, len, |
| value, user_data); |
| }, |
| nullptr); |
| } else { |
| LOG(WARNING) << __func__ |
| << ": server can only report HAS features, other " |
| "functionality is disabled"; |
| } |
| |
| return true; |
| } |
| |
| bool OnHasServiceFound(const gatt::Service& service, void* context) { |
| DLOG(INFO) << __func__; |
| |
| auto* device = static_cast<HasDevice*>(context); |
| |
| /* Initially validate and store GATT service discovery data */ |
| if (!CacheAttributeHandles(service, device)) return false; |
| |
| /* If deatails are loaded from storage we are done here */ |
| if (LoadHasDetailsFromStorage(device)) return true; |
| |
| /* No storred details - read all the details and validate */ |
| return StartInitialHasDetailsReadAndValidation(service, device); |
| } |
| |
| /* These below are all generic event handlers calling in HAS specific code. */ |
| void GattcCallback(tBTA_GATTC_EVT event, tBTA_GATTC* p_data) { |
| DLOG(INFO) << __func__ << ": event = " << static_cast<int>(event); |
| |
| switch (event) { |
| case BTA_GATTC_DEREG_EVT: |
| break; |
| |
| case BTA_GATTC_OPEN_EVT: |
| OnGattConnected(p_data->open); |
| break; |
| |
| case BTA_GATTC_CLOSE_EVT: |
| OnGattDisconnected(p_data->close); |
| break; |
| |
| case BTA_GATTC_SEARCH_CMPL_EVT: |
| OnGattServiceSearchComplete(p_data->search_cmpl); |
| break; |
| |
| case BTA_GATTC_NOTIF_EVT: |
| OnGattNotification(p_data->notify); |
| break; |
| |
| case BTA_GATTC_ENC_CMPL_CB_EVT: |
| OnLeEncryptionComplete(p_data->enc_cmpl.remote_bda, |
| BTM_IsEncrypted(p_data->enc_cmpl.remote_bda, BT_TRANSPORT_LE)); |
| break; |
| |
| case BTA_GATTC_SRVC_CHG_EVT: |
| OnGattServiceChangeEvent(p_data->remote_bda); |
| break; |
| |
| case BTA_GATTC_SRVC_DISC_DONE_EVT: |
| OnGattServiceDiscoveryDoneEvent(p_data->remote_bda); |
| break; |
| |
| default: |
| break; |
| } |
| } |
| |
| void OnGattConnected(const tBTA_GATTC_OPEN& evt) { |
| LOG_INFO("%s, conn_id=0x%04x, transport=%s, status=%s(0x%02x)", |
| ADDRESS_TO_LOGGABLE_CSTR(evt.remote_bda), evt.conn_id, |
| bt_transport_text(evt.transport).c_str(), |
| gatt_status_text(evt.status).c_str(), evt.status); |
| |
| if (evt.transport != BT_TRANSPORT_LE) { |
| LOG_WARN("Only LE connection is allowed (transport %s)", |
| bt_transport_text(evt.transport).c_str()); |
| BTA_GATTC_Close(evt.conn_id); |
| return; |
| } |
| |
| auto device = std::find_if(devices_.begin(), devices_.end(), |
| HasDevice::MatchAddress(evt.remote_bda)); |
| if (device == devices_.end()) { |
| LOG(WARNING) << "Skipping unknown device, address=" |
| << ADDRESS_TO_LOGGABLE_STR(evt.remote_bda); |
| BTA_GATTC_Close(evt.conn_id); |
| return; |
| } |
| |
| if (evt.status != GATT_SUCCESS) { |
| if (!device->is_connecting_actively) { |
| // acceptlist connection failed, that's ok. |
| return; |
| } |
| |
| LOG(WARNING) << "Failed to connect to server device"; |
| devices_.erase(device); |
| callbacks_->OnConnectionState(ConnectionState::DISCONNECTED, |
| evt.remote_bda); |
| return; |
| } |
| |
| device->conn_id = evt.conn_id; |
| |
| if (BTM_SecIsSecurityPending(device->addr)) { |
| /* if security collision happened, wait for encryption done |
| * (BTA_GATTC_ENC_CMPL_CB_EVT) |
| */ |
| return; |
| } |
| |
| /* verify bond */ |
| if (BTM_IsEncrypted(device->addr, BT_TRANSPORT_LE)) { |
| /* if link has been encrypted */ |
| OnEncrypted(*device); |
| return; |
| } |
| |
| int result = BTM_SetEncryption(device->addr, BT_TRANSPORT_LE, nullptr, |
| nullptr, BTM_BLE_SEC_ENCRYPT); |
| |
| LOG_INFO("Encryption required for %s. Request result: 0x%02x", |
| ADDRESS_TO_LOGGABLE_CSTR(device->addr), result); |
| |
| if (result == BTM_ERR_KEY_MISSING) { |
| LOG_ERROR("Link key unknown for %s, disconnect profile", |
| ADDRESS_TO_LOGGABLE_CSTR(device->addr)); |
| BTA_GATTC_Close(device->conn_id); |
| } |
| } |
| |
| void OnGattDisconnected(const tBTA_GATTC_CLOSE& evt) { |
| auto device = std::find_if(devices_.begin(), devices_.end(), |
| HasDevice::MatchAddress(evt.remote_bda)); |
| if (device == devices_.end()) { |
| LOG(WARNING) << "Skipping unknown device disconnect, conn_id=" |
| << loghex(evt.conn_id); |
| return; |
| } |
| DLOG(INFO) << __func__ << ": device=" |
| << ADDRESS_TO_LOGGABLE_STR(device->addr) |
| << ": reason=" << loghex(static_cast<int>(evt.reason)); |
| |
| /* Don't notify disconnect state for background connection that failed */ |
| if (device->is_connecting_actively || device->isGattServiceValid()) |
| callbacks_->OnConnectionState(ConnectionState::DISCONNECTED, |
| evt.remote_bda); |
| |
| auto peer_disconnected = (evt.reason == GATT_CONN_TIMEOUT) || |
| (evt.reason == GATT_CONN_TERMINATE_PEER_USER); |
| DoDisconnectCleanUp(*device, peer_disconnected ? false : true); |
| |
| /* Connect in background - is this ok? */ |
| if (peer_disconnected) |
| BTA_GATTC_Open(gatt_if_, device->addr, BTM_BLE_BKG_CONNECT_ALLOW_LIST, |
| false); |
| } |
| |
| void OnGattServiceSearchComplete(const tBTA_GATTC_SEARCH_CMPL& evt) { |
| auto device = GetDevice(evt.conn_id); |
| if (!device) { |
| LOG(WARNING) << "Skipping unknown device, conn_id=" |
| << loghex(evt.conn_id); |
| return; |
| } |
| |
| DLOG(INFO) << __func__; |
| |
| /* verify link is encrypted */ |
| if (!BTM_IsEncrypted(device->addr, BT_TRANSPORT_LE)) { |
| LOG_WARN("Device not yet bonded - waiting for encryption"); |
| return; |
| } |
| |
| /* Ignore if our service data is valid (service discovery initiated by |
| * someone else?) |
| */ |
| if (!device->isGattServiceValid()) { |
| if (evt.status != GATT_SUCCESS) { |
| LOG(ERROR) << __func__ << ": Service discovery failed"; |
| BTA_GATTC_Close(device->conn_id); |
| return; |
| } |
| |
| const std::list<gatt::Service>* all_services = |
| BTA_GATTC_GetServices(device->conn_id); |
| |
| auto service = |
| std::find_if(all_services->begin(), all_services->end(), |
| [](const gatt::Service& svc) { |
| return svc.uuid == kUuidHearingAccessService; |
| }); |
| if (service == all_services->end()) { |
| LOG(ERROR) << "No service found"; |
| BTA_GATTC_Close(device->conn_id); |
| return; |
| } |
| |
| /* Call the service specific verifier callback */ |
| if (!instance->OnHasServiceFound(*service, &(*device))) { |
| LOG(ERROR) << "Not a valid service!"; |
| BTA_GATTC_Close(device->conn_id); |
| return; |
| } |
| } |
| } |
| |
| void OnGattNotification(const tBTA_GATTC_NOTIFY& evt) { |
| /* Reject invalid lengths */ |
| if (evt.len > GATT_MAX_ATTR_LEN) { |
| LOG(ERROR) << __func__ << ": rejected BTA_GATTC_NOTIF_EVT. is_notify = " |
| << evt.is_notify << ", len=" << static_cast<int>(evt.len); |
| } |
| if (!evt.is_notify) BTA_GATTC_SendIndConfirm(evt.conn_id, evt.cid); |
| |
| OnHasNotification(evt.conn_id, evt.handle, evt.len, evt.value); |
| } |
| |
| void OnLeEncryptionComplete(const RawAddress& address, bool success) { |
| DLOG(INFO) << __func__ << ": " << ADDRESS_TO_LOGGABLE_STR(address); |
| |
| auto device = std::find_if(devices_.begin(), devices_.end(), |
| HasDevice::MatchAddress(address)); |
| if (device == devices_.end()) { |
| LOG(WARNING) << "Skipping unknown device" |
| << ADDRESS_TO_LOGGABLE_STR(address); |
| return; |
| } |
| |
| if (!success) { |
| LOG(ERROR) << "Encryption failed for device " |
| << ADDRESS_TO_LOGGABLE_STR(address); |
| |
| BTA_GATTC_Close(device->conn_id); |
| return; |
| } |
| |
| if (device->isGattServiceValid()) { |
| instance->OnEncrypted(*device); |
| } else { |
| BTA_GATTC_ServiceSearchRequest(device->conn_id, |
| &kUuidHearingAccessService); |
| } |
| } |
| |
| void ClearDeviceInformationAndStartSearch(HasDevice* device) { |
| if (!device) { |
| LOG_ERROR("Device is null"); |
| return; |
| } |
| |
| LOG_INFO("%s", ADDRESS_TO_LOGGABLE_CSTR(device->addr)); |
| |
| if (!device->isGattServiceValid()) { |
| LOG_INFO("Service already invalidated"); |
| return; |
| } |
| |
| /* Invalidate service discovery results */ |
| DeregisterNotifications(*device); |
| BtaGattQueue::Clean(device->conn_id); |
| device->ClearSvcData(); |
| btif_storage_remove_leaudio_has(device->addr); |
| BTA_GATTC_ServiceSearchRequest(device->conn_id, &kUuidHearingAccessService); |
| } |
| |
| void OnGattServiceChangeEvent(const RawAddress& address) { |
| auto device = std::find_if(devices_.begin(), devices_.end(), |
| HasDevice::MatchAddress(address)); |
| if (device == devices_.end()) { |
| LOG(WARNING) << "Skipping unknown device" << address; |
| return; |
| } |
| LOG_INFO("%s", ADDRESS_TO_LOGGABLE_CSTR(address)); |
| ClearDeviceInformationAndStartSearch(&(*device)); |
| } |
| |
| void OnGattServiceDiscoveryDoneEvent(const RawAddress& address) { |
| auto device = std::find_if(devices_.begin(), devices_.end(), |
| HasDevice::MatchAddress(address)); |
| if (device == devices_.end()) { |
| LOG(WARNING) << "Skipping unknown device" |
| << ADDRESS_TO_LOGGABLE_STR(address); |
| return; |
| } |
| |
| DLOG(INFO) << __func__ << ": address=" |
| << ADDRESS_TO_LOGGABLE_STR(address); |
| |
| if (!device->isGattServiceValid()) |
| BTA_GATTC_ServiceSearchRequest(device->conn_id, |
| &kUuidHearingAccessService); |
| } |
| |
| static uint16_t FindCccHandle(uint16_t conn_id, uint16_t char_handle) { |
| const gatt::Characteristic* p_char = |
| BTA_GATTC_GetCharacteristic(conn_id, char_handle); |
| if (!p_char) { |
| LOG(WARNING) << __func__ << ": No such characteristic: " << char_handle; |
| return GAP_INVALID_HANDLE; |
| } |
| |
| for (const gatt::Descriptor& desc : p_char->descriptors) { |
| if (desc.uuid == Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG)) |
| return desc.handle; |
| } |
| |
| return GAP_INVALID_HANDLE; |
| } |
| |
| void SubscribeForNotifications( |
| uint16_t conn_id, const RawAddress& address, uint16_t value_handle, |
| uint16_t ccc_handle, |
| uint16_t ccc_val = GATT_CHAR_CLIENT_CONFIG_NOTIFICATION) { |
| if (value_handle != GAP_INVALID_HANDLE) { |
| tGATT_STATUS register_status = |
| BTA_GATTC_RegisterForNotifications(gatt_if_, address, value_handle); |
| DLOG(INFO) << __func__ << ": BTA_GATTC_RegisterForNotifications, status=" |
| << loghex(+register_status) |
| << " value=" << loghex(value_handle) |
| << " ccc=" << loghex(ccc_handle); |
| |
| if (register_status != GATT_SUCCESS) return; |
| } |
| |
| std::vector<uint8_t> value(2); |
| uint8_t* value_ptr = value.data(); |
| UINT16_TO_STREAM(value_ptr, ccc_val); |
| BtaGattQueue::WriteDescriptor( |
| conn_id, ccc_handle, std::move(value), GATT_WRITE, |
| [](uint16_t conn_id, tGATT_STATUS status, uint16_t value_handle, |
| uint16_t len, const uint8_t* value, void* data) { |
| if (instance) |
| instance->OnGattWriteCcc(conn_id, status, value_handle, data); |
| }, |
| HasGattOpContext(HasGattOpContext::kContextFlagsEnableNotification)); |
| } |
| |
| uint8_t gatt_if_; |
| bluetooth::has::HasClientCallbacks* callbacks_; |
| std::list<HasDevice> devices_; |
| std::list<HasCtpOp> pending_operations_; |
| |
| typedef std::map<decltype(HasCtpOp::op_id), HasCtpGroupOpCoordinator> |
| has_operation_timeouts_t; |
| has_operation_timeouts_t pending_group_operation_timeouts_; |
| }; |
| |
| } // namespace |
| |
| alarm_t* HasCtpGroupOpCoordinator::operation_timeout_timer = nullptr; |
| size_t HasCtpGroupOpCoordinator::ref_cnt = 0u; |
| alarm_callback_t HasCtpGroupOpCoordinator::cb = [](void*) {}; |
| |
| void HasClient::Initialize(bluetooth::has::HasClientCallbacks* callbacks, |
| base::Closure initCb) { |
| std::scoped_lock<std::mutex> lock(instance_mutex); |
| if (instance) { |
| LOG(ERROR) << "Already initialized!"; |
| return; |
| } |
| |
| HasCtpGroupOpCoordinator::Initialize([](void* p) { |
| if (instance) instance->OnGroupOpCoordinatorTimeout(p); |
| }); |
| instance = new HasClientImpl(callbacks, initCb); |
| } |
| |
| bool HasClient::IsHasClientRunning() { return instance; } |
| |
| HasClient* HasClient::Get(void) { |
| CHECK(instance); |
| return instance; |
| }; |
| |
| void HasClient::AddFromStorage(const RawAddress& addr, uint8_t features, |
| uint16_t is_acceptlisted) { |
| if (!instance) { |
| LOG(ERROR) << "Not initialized yet"; |
| } |
| |
| instance->AddFromStorage(addr, features, is_acceptlisted); |
| }; |
| |
| void HasClient::CleanUp() { |
| std::scoped_lock<std::mutex> lock(instance_mutex); |
| HasClientImpl* ptr = instance; |
| instance = nullptr; |
| |
| if (ptr) { |
| ptr->CleanUp(); |
| delete ptr; |
| } |
| |
| HasCtpGroupOpCoordinator::Cleanup(); |
| }; |
| |
| void HasClient::DebugDump(int fd) { |
| std::scoped_lock<std::mutex> lock(instance_mutex); |
| dprintf(fd, "Hearing Access Service Client:\n"); |
| if (instance) |
| instance->Dump(fd); |
| else |
| dprintf(fd, " no instance\n\n"); |
| } |