2138 lines
74 KiB
C++
2138 lines
74 KiB
C++
/*
|
|
* Copyright 2021 HIMSA II K/S - www.himsa.com.
|
|
* Represented by EHIMA - www.ehima.com
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include <base/bind.h>
|
|
#include <base/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 <string>
|
|
#include <vector>
|
|
|
|
#include "bta_csis_api.h"
|
|
#include "bta_gatt_api.h"
|
|
#include "bta_gatt_queue.h"
|
|
#include "bta_groups.h"
|
|
#include "bta_has_api.h"
|
|
#include "bta_le_audio_uuids.h"
|
|
#include "btm_int.h"
|
|
#include "btm_sec.h"
|
|
#include "device/include/controller.h"
|
|
#include "gap_api.h"
|
|
#include "gatt_api.h"
|
|
#include "has_types.h"
|
|
#include "osi/include/log.h"
|
|
#include "osi/include/osi.h"
|
|
#include "osi/include/properties.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);
|
|
|
|
extern bool gatt_profile_get_eatt_support(const RawAddress& remote_bda);
|
|
|
|
namespace {
|
|
class HasClientImpl;
|
|
HasClientImpl* instance;
|
|
|
|
/**
|
|
* -----------------------------------------------------------------------------
|
|
* 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;
|
|
|
|
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 << " 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
|
|
<< ", 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;
|
|
|
|
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 << " 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" << 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);
|
|
}
|
|
|
|
/* 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", device->addr.ToString().c_str());
|
|
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", device->addr.ToString().c_str());
|
|
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", device->addr.ToString().c_str());
|
|
ClearDeviceInformationAndStartSearch(device);
|
|
} else {
|
|
LOG_ERROR("Devices %s: Control point not usable. Disconnecting!",
|
|
device->addr.ToString().c_str());
|
|
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", device->addr.ToString().c_str());
|
|
ClearDeviceInformationAndStartSearch(device);
|
|
} else {
|
|
LOG_ERROR("Devices %s: Control point not usable. Disconnecting!",
|
|
device->addr.ToString().c_str());
|
|
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: "
|
|
<< 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: " << 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: " << 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;
|
|
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;
|
|
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;
|
|
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", device.addr.ToString().c_str());
|
|
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__ << ": " << 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:" << 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", device->addr.ToString().c_str());
|
|
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",
|
|
device->addr.ToString().c_str());
|
|
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",
|
|
device->addr.ToString().c_str());
|
|
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", device.addr.ToString().c_str());
|
|
|
|
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=" << 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=" << 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) {
|
|
DLOG(INFO) << __func__ << ": address=" << evt.remote_bda
|
|
<< ", conn_id=" << evt.conn_id;
|
|
|
|
auto device = std::find_if(devices_.begin(), devices_.end(),
|
|
HasDevice::MatchAddress(evt.remote_bda));
|
|
if (device == devices_.end()) {
|
|
LOG(WARNING) << "Skipping unknown device, address=" << 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 */
|
|
if (device->isGattServiceValid()) {
|
|
instance->OnEncrypted(*device);
|
|
} else {
|
|
BTA_GATTC_ServiceSearchRequest(device->conn_id,
|
|
&kUuidHearingAccessService);
|
|
}
|
|
return;
|
|
}
|
|
|
|
int result = BTM_SetEncryption(
|
|
evt.remote_bda, BT_TRANSPORT_LE,
|
|
[](const RawAddress* bd_addr, tBT_TRANSPORT transport, void* p_ref_data,
|
|
tBTM_STATUS status) {
|
|
if (instance)
|
|
instance->OnLeEncryptionComplete(*bd_addr, status == BTM_SUCCESS);
|
|
},
|
|
nullptr, BTM_BLE_SEC_ENCRYPT);
|
|
|
|
DLOG(INFO) << __func__ << ": Encryption request result: " << result;
|
|
}
|
|
|
|
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=" << 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__;
|
|
|
|
/* 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;
|
|
|
|
auto device = std::find_if(devices_.begin(), devices_.end(),
|
|
HasDevice::MatchAddress(address));
|
|
if (device == devices_.end()) {
|
|
LOG(WARNING) << "Skipping unknown device" << address;
|
|
return;
|
|
}
|
|
|
|
if (!success) {
|
|
LOG(ERROR) << "Encryption failed for device " << 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", device->addr.ToString().c_str());
|
|
|
|
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.ToString().c_str());
|
|
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;
|
|
return;
|
|
}
|
|
|
|
DLOG(INFO) << __func__ << ": address=" << 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) {
|
|
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() {
|
|
HasClientImpl* ptr = instance;
|
|
instance = nullptr;
|
|
|
|
if (ptr) {
|
|
ptr->CleanUp();
|
|
delete ptr;
|
|
}
|
|
|
|
HasCtpGroupOpCoordinator::Cleanup();
|
|
};
|
|
|
|
void HasClient::DebugDump(int fd) {
|
|
dprintf(fd, "Hearing Access Service Client:\n");
|
|
if (instance)
|
|
instance->Dump(fd);
|
|
else
|
|
dprintf(fd, " no instance\n\n");
|
|
}
|