chromium/chrome/services/sharing/nearby/platform/ble_v2_gatt_client.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/services/sharing/nearby/platform/ble_v2_gatt_client.h"

#include "base/logging.h"
#include "base/notreached.h"
#include "base/synchronization/waitable_event.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/services/sharing/nearby/platform/bluetooth_utils.h"
#include "chrome/services/sharing/nearby/platform/nearby_platform_metrics.h"

namespace {

using BluetoothProperties = device::BluetoothGattCharacteristic::Properties;
using BluetoothPermissions = device::BluetoothGattCharacteristic::Permissions;
using BluetoothProperty = device::BluetoothGattCharacteristic::Property;
using BluetoothPermission = device::BluetoothGattCharacteristic::Permission;
using NearbyPermission = nearby::api::ble_v2::GattCharacteristic::Permission;
using NearbyProperty = nearby::api::ble_v2::GattCharacteristic::Property;

void CancelPendingTasks(
    base::flat_set<raw_ptr<base::WaitableEvent>>& events_to_cancel) {
  if (!events_to_cancel.empty()) {
    VLOG(1) << __func__ << ": Canceling " << events_to_cancel.size()
            << " pending calls.";
  }

  for (base::WaitableEvent* event : std::move(events_to_cancel)) {
    event->Signal();
  }
}

bool WereAllExpectedCharacteristicsFound(
    const std::vector<bluetooth::mojom::CharacteristicInfoPtr>&
        found_characteristics,
    const std::vector<nearby::Uuid>& expected_characteristic_uuids) {
  std::set<nearby::Uuid> found_characteristic_uuids;
  for (const auto& characteristic : found_characteristics) {
    found_characteristic_uuids.insert(
        nearby::chrome::BluetoothUuidToNearbyUuid(characteristic->uuid));
  }

  for (const auto& uuid : expected_characteristic_uuids) {
    if (found_characteristic_uuids.find(uuid) ==
        found_characteristic_uuids.end()) {
      return false;
    }
  }

  return true;
}

NearbyPermission ConvertPermission(BluetoothPermissions permissions) {
  if ((permissions & BluetoothPermission::PERMISSION_READ) != 0) {
    return NearbyPermission::kRead;
  }

  if ((permissions & BluetoothPermission::PERMISSION_WRITE) != 0) {
    return NearbyPermission::kWrite;
  }

  if ((permissions & BluetoothPermission::PERMISSION_WRITE) != 0) {
    return NearbyPermission::kWrite;
  }

  return NearbyPermission::kNone;
}

NearbyProperty ConvertProperty(BluetoothProperties properties) {
  if ((properties & BluetoothProperty::PROPERTY_READ) != 0) {
    return NearbyProperty::kRead;
  }

  if ((properties & BluetoothProperty::PROPERTY_WRITE) != 0) {
    return NearbyProperty::kWrite;
  }

  if ((properties & BluetoothProperty::PROPERTY_INDICATE) != 0) {
    return NearbyProperty::kIndicate;
  }

  if ((properties & BluetoothProperty::PROPERTY_NOTIFY) != 0) {
    return NearbyProperty::kNotify;
  }

  return NearbyProperty::kNone;
}

std::string_view GattResultToString(bluetooth::mojom::GattResult result) {
  switch (result) {
    case bluetooth::mojom::GattResult::SUCCESS:
      return "Success";
    case bluetooth::mojom::GattResult::UNKNOWN:
      return "Unknown";
    case bluetooth::mojom::GattResult::FAILED:
      return "Failed";
    case bluetooth::mojom::GattResult::IN_PROGRESS:
      return "In Progress";
    case bluetooth::mojom::GattResult::INVALID_LENGTH:
      return "Invalid Length";
    case bluetooth::mojom::GattResult::NOT_PERMITTED:
      return "Not Permitted";
    case bluetooth::mojom::GattResult::NOT_AUTHORIZED:
      return "Not Authorized";
    case bluetooth::mojom::GattResult::NOT_PAIRED:
      return "Not Paired";
    case bluetooth::mojom::GattResult::NOT_SUPPORTED:
      return "Not Supported";
    case bluetooth::mojom::GattResult::SERVICE_NOT_FOUND:
      return "Service Not Found";
    case bluetooth::mojom::GattResult::CHARACTERISTIC_NOT_FOUND:
      return "Characteristic Not Found";
    case bluetooth::mojom::GattResult::DESCRIPTOR_NOT_FOUND:
      return "Descriptor Not Found";
  }
}

}  // namespace

namespace nearby::chrome {

std::unique_ptr<BleV2GattClient::GattService>
BleV2GattClient::GattService::Factory::Create() {
  return base::WrapUnique(new BleV2GattClient::GattService());
}

BleV2GattClient::GattService::Factory::~Factory() = default;

BleV2GattClient::GattService::GattService() = default;
BleV2GattClient::GattService::~GattService() = default;

BleV2GattClient::BleV2GattClient(
    mojo::PendingRemote<bluetooth::mojom::Device> device,
    std::unique_ptr<GattService::Factory> gatt_service_factory)
    : task_runner_(
          base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()})),
      gatt_service_factory_(std::move(gatt_service_factory)),
      remote_device_(std::move(device), /*bind_task_runner=*/task_runner_) {
  CHECK(remote_device_.is_bound());

  remote_device_.set_disconnect_handler(
      base::BindOnce(&BleV2GattClient::OnMojoDisconnect,
                     weak_ptr_factory_.GetWeakPtr()),
      task_runner_);
}

BleV2GattClient::~BleV2GattClient() {
  // For thread safety, shut down on the |task_runner_|.
  base::WaitableEvent shutdown_waitable_event;
  task_runner_->PostTask(FROM_HERE, base::BindOnce(&BleV2GattClient::Shutdown,
                                                   base::Unretained(this),
                                                   &shutdown_waitable_event));
  shutdown_waitable_event.Wait();
}

bool BleV2GattClient::DiscoverServiceAndCharacteristics(
    const Uuid& service_uuid,
    const std::vector<Uuid>& characteristic_uuids) {
  VLOG(1) << __func__;

  // Nearby Connections expects `DiscoverServiceAndCharacteristics()` to be
  // a blocking call in order to (1) discover a GATT service at `service_uuid`
  // and (2) discover GATT characteristics in the GATT service at
  // `characteristic_uuids` in the GATT connection, represented by
  // `remote_device_`.
  //
  // First, kick off (1) Discover a GATT service at `service_uuid` by retrieving
  // all services on `remote_device_` if `BleV2GattClient` has not already, and
  // parsing for a match to  `service_uuid`. If there isn't a match, return a
  // failure.
  if (!have_gatt_services_been_discovered_) {
    VLOG(1) << __func__
            << ": attempting to discover services on the remote device";
    base::WaitableEvent discover_services_waitable_event;
    task_runner_->PostTask(FROM_HERE,
                           base::BindOnce(&BleV2GattClient::DoDiscoverServices,
                                          base::Unretained(this),
                                          &discover_services_waitable_event));
    base::ScopedAllowBaseSyncPrimitives allow_wait;
    discover_services_waitable_event.Wait();
    have_gatt_services_been_discovered_ = true;
  }

  auto it =
      uuid_to_discovered_gatt_services_map_.find(std::string(service_uuid));
  if (it == uuid_to_discovered_gatt_services_map_.end()) {
    LOG(WARNING) << __func__ << ": no match for " << std::string(service_uuid)
                 << " in " << uuid_to_discovered_gatt_services_map_.size()
                 << " number of service UUIDs";
    return false;
  }

  // Next, kick off (2) discover GATT characteristics in the GATT
  // service at `characteristic_uuids` by fetching all GATT characteristics in
  // `service_uuid` if we haven't already. Parse the list of GATT
  // characteristics for matches with all of the characteristic UUIDs in
  // `characteristic_uuids`; if not all match, return false.
  //
  // This GATT characteristic discovery needs to be done for multiple GATT
  // services with the same UUID if they exist in the case of a remote GATT
  // server that contains duplicate services with the same UUID, but each
  // contain a different set of characteristics.
  for (const auto& gatt_service : it->second) {
    if (!gatt_service->characteristics.has_value()) {
      VLOG(1)
          << __func__
          << ": attempting to discover characteristics on the remote device "
             "for service uuid = "
          << std::string(service_uuid)
          << ", service id = " << gatt_service->service_info->id;
      base::WaitableEvent get_characteristics_waitable_event;
      task_runner_->PostTask(
          FROM_HERE, base::BindOnce(&BleV2GattClient::DoGetCharacteristics,
                                    base::Unretained(this),
                                    /*gatt_service=*/gatt_service.get(),
                                    &get_characteristics_waitable_event));
      base::ScopedAllowBaseSyncPrimitives allow_wait;
      get_characteristics_waitable_event.Wait();

      if (!gatt_service->characteristics.has_value()) {
        LOG(WARNING) << __func__
                     << ": failed to retrieve characteristics on the remote "
                        "device for service uuid = "
                     << std::string(service_uuid);
        continue;
      }
    }

    if (WereAllExpectedCharacteristicsFound(
            /*found_characteristics=*/gatt_service->characteristics.value(),
            /*expected_characteristic_uuids=*/characteristic_uuids)) {
      VLOG(1) << __func__ << " found successfully";
      return true;
    }
  }

  return false;
}

std::optional<api::ble_v2::GattCharacteristic>
BleV2GattClient::GetCharacteristic(const Uuid& service_uuid,
                                   const Uuid& characteristic_uuid) {
  VLOG(1) << __func__;

  auto characteristic_info =
      GetCharacteristicInfoMojom(service_uuid, characteristic_uuid);
  if (!characteristic_info) {
    LOG(WARNING) << __func__
                 << ": no match for characteristic in the GATT client";
    return std::nullopt;
  }

  // The current implementation of BLE V2 in Nearby Connections only supports a
  // single permission or property type for a characteristic, even though the
  // Bluetooth Adapter in the platform layer can support multiple properties
  // using bitwise operations. `BleV2GattClient` converts the returned
  // properties and permissions to a single
  // nearby::api::ble_v2::GattCharacteristic::Property/Permission.
  CHECK(characteristic_info->first);
  api::ble_v2::GattCharacteristic gatt_characteristic = {
      characteristic_uuid, service_uuid,
      ConvertPermission(characteristic_info->first->permissions),
      ConvertProperty(characteristic_info->first->properties)};
  return gatt_characteristic;
}

std::optional<std::string> BleV2GattClient::ReadCharacteristic(
    const api::ble_v2::GattCharacteristic& characteristic) {
  auto characteristic_info = GetCharacteristicInfoMojom(
      characteristic.service_uuid, characteristic.uuid);
  if (!characteristic_info) {
    LOG(WARNING) << __func__
                 << ": no match for characteristic in the GATT client";
    metrics::RecordGattClientReadCharacteristicResult(/*success=*/false);
    return std::nullopt;
  }

  VLOG(1) << __func__ << ": attempting to read from characteristic at UUID = "
          << std::string(characteristic.uuid);

  CHECK(characteristic_info->first);
  std::optional<std::string> read_characteristic_result;
  base::WaitableEvent read_characteristic_waitable_event;
  task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(
          &BleV2GattClient::DoReadCharacteristic, base::Unretained(this),
          /*service_id=*/characteristic_info->second,
          /*characteristic_id=*/characteristic_info->first->id,
          &read_characteristic_result, &read_characteristic_waitable_event));
  base::ScopedAllowBaseSyncPrimitives allow_wait;
  read_characteristic_waitable_event.Wait();

  metrics::RecordGattClientReadCharacteristicResult(
      /*success=*/read_characteristic_result.has_value());
  return read_characteristic_result;
}

bool BleV2GattClient::WriteCharacteristic(
    const api::ble_v2::GattCharacteristic& characteristic,
    std::string_view value,
    WriteType type) {
  // TODO(b/311430390): Implement this function.
  NOTIMPLEMENTED();
  return false;
}

bool BleV2GattClient::SetCharacteristicSubscription(
    const api::ble_v2::GattCharacteristic& characteristic,
    bool enable,
    absl::AnyInvocable<void(std::string_view value)>
        on_characteristic_changed_cb) {
  // TODO(b/311430390): Implement this function.
  NOTIMPLEMENTED();
  return false;
}

void BleV2GattClient::Disconnect() {
  remote_device_->Disconnect();
}

void BleV2GattClient::DoDiscoverServices(
    base::WaitableEvent* discover_services_waitable_event) {
  CHECK(task_runner_->RunsTasksInCurrentSequence());
  pending_discover_services_waitable_events_.insert(
      discover_services_waitable_event);
  CHECK(remote_device_.is_bound());

  remote_device_->GetServices(
      base::BindOnce(&BleV2GattClient::OnGetGattServices,
                     base::Unretained(this), discover_services_waitable_event));
}

void BleV2GattClient::OnGetGattServices(
    base::WaitableEvent* discover_services_waitable_event,
    std::vector<bluetooth::mojom::ServiceInfoPtr> services) {
  CHECK(task_runner_->RunsTasksInCurrentSequence());
  if (!pending_discover_services_waitable_events_.contains(
          discover_services_waitable_event)) {
    // The event has already been signaled.
    return;
  }

  VLOG(1) << __func__ << ": retrieved " << services.size()
          << ": GATT services from the remote device";

  // Store all returned `services` in case future calls to
  // `DiscoverServiceAndCharacteristics()` request a different service UUID to
  // prevent having to query the services again from the remote device. In the
  // case Android is the advertising device, there may be multiple services
  // with the same UUID; BleV2GattClient needs to store both objects in the
  // case the requested characteristics is only in one of the duplicated
  // services.
  for (const auto& service : services) {
    Uuid nearby_service_uuid = BluetoothUuidToNearbyUuid(service->uuid);
    // Using [] operators is safe here because there is no concern for
    // creating an element on accident - we are intending to create an element
    // if it doesn't exist, and inserting a new GATT service into the vector.
    uuid_to_discovered_gatt_services_map_[std::string(nearby_service_uuid)]
        .emplace_back(gatt_service_factory_->Create());
    uuid_to_discovered_gatt_services_map_[std::string(nearby_service_uuid)]
        .back()
        ->service_info = service.Clone();
  }

  if (!discover_services_waitable_event->IsSignaled()) {
    discover_services_waitable_event->Signal();
    pending_discover_services_waitable_events_.erase(
        discover_services_waitable_event);
  }
}

void BleV2GattClient::DoGetCharacteristics(
    GattService* gatt_service,
    base::WaitableEvent* get_characteristics_waitable_event) {
  CHECK(task_runner_->RunsTasksInCurrentSequence());
  CHECK(remote_device_.is_bound());

  pending_get_characteristics_waitable_events_.insert(
      get_characteristics_waitable_event);
  remote_device_->GetCharacteristics(
      gatt_service->service_info->id,
      base::BindOnce(&BleV2GattClient::OnGetCharacteristics,
                     base::Unretained(this), gatt_service,
                     get_characteristics_waitable_event));
}

void BleV2GattClient::OnGetCharacteristics(
    GattService* gatt_service,
    base::WaitableEvent* get_characteristics_waitable_event,
    std::optional<std::vector<bluetooth::mojom::CharacteristicInfoPtr>>
        characteristics) {
  CHECK(task_runner_->RunsTasksInCurrentSequence());
  if (!pending_get_characteristics_waitable_events_.contains(
          get_characteristics_waitable_event)) {
    // The event has already been signaled.
    return;
  }

  if (characteristics.has_value()) {
    VLOG(1) << __func__ << ": got " << characteristics.value().size()
            << " characteristics";
    gatt_service->characteristics = std::move(characteristics);
  } else {
    LOG(WARNING) << __func__ << ": failed to get characteristics";
  }

  if (!get_characteristics_waitable_event->IsSignaled()) {
    get_characteristics_waitable_event->Signal();
    pending_get_characteristics_waitable_events_.erase(
        get_characteristics_waitable_event);
  }
}

void BleV2GattClient::DoReadCharacteristic(
    const std::string& service_id,
    const std::string& characteristic_id,
    std::optional<std::string>* read_characteristic_result,
    base::WaitableEvent* read_characteristic_waitable_event) {
  CHECK(task_runner_->RunsTasksInCurrentSequence());
  pending_read_characteristic_waitable_events_.insert(
      read_characteristic_waitable_event);
  CHECK(remote_device_.is_bound());

  remote_device_->ReadValueForCharacteristic(
      service_id, characteristic_id,
      base::BindOnce(
          &BleV2GattClient::OnReadCharacteristic, base::Unretained(this),
          /*gatt_read_characteristic_start_time=*/base::TimeTicks::Now(),
          read_characteristic_result, read_characteristic_waitable_event));
}

void BleV2GattClient::OnReadCharacteristic(
    base::TimeTicks gatt_read_characteristic_start_time,
    std::optional<std::string>* read_characteristic_result,
    base::WaitableEvent* read_characteristic_waitable_event,
    bluetooth::mojom::GattResult result,
    const std::optional<std::vector<uint8_t>>& value) {
  CHECK(task_runner_->RunsTasksInCurrentSequence());
  if (!pending_read_characteristic_waitable_events_.contains(
          read_characteristic_waitable_event)) {
    // The event has already been signaled.
    return;
  }

  VLOG(1) << __func__ << ": Result = " << GattResultToString(result);

  if (result != bluetooth::mojom::GattResult::SUCCESS) {
    CHECK(!value.has_value()) << __func__
                              << ": expected no value read from characteristic "
                                 "due to failure returned";
    *read_characteristic_result = std::nullopt;
    metrics::RecordGattClientReadCharacteristicFailureReason(result);
  } else {
    CHECK(value.has_value())
        << __func__
        << ": expected value read from characteristic due to success returned";
    *read_characteristic_result =
        std::string(value.value().begin(), value.value().end());
    metrics::RecordGattClientReadCharacteristicDuration(
        /*duration=*/base::TimeTicks::Now() -
        gatt_read_characteristic_start_time);
  }

  if (!read_characteristic_waitable_event->IsSignaled()) {
    read_characteristic_waitable_event->Signal();
    pending_read_characteristic_waitable_events_.erase(
        read_characteristic_waitable_event);
  }
}

std::optional<std::pair<bluetooth::mojom::CharacteristicInfoPtr, std::string>>
BleV2GattClient::GetCharacteristicInfoMojom(const Uuid& service_uuid,
                                            const Uuid& characteristic_uuid) {
  VLOG(1) << __func__;

  auto services_it =
      uuid_to_discovered_gatt_services_map_.find(std::string(service_uuid));
  if (services_it == uuid_to_discovered_gatt_services_map_.end()) {
    LOG(WARNING) << __func__ << ": no match for service at UUID"
                 << std::string(service_uuid) << " in "
                 << uuid_to_discovered_gatt_services_map_.size()
                 << " number of discovered service UUIDs";
    return std::nullopt;
  }

  for (const auto& gatt_service : services_it->second) {
    const auto& characteristics = gatt_service->characteristics;
    if (!characteristics.has_value()) {
      LOG(WARNING) << __func__ << ": characteristics have no value";
      continue;
    }

    auto char_it = std::find_if(
        characteristics.value().begin(), characteristics.value().end(),
        [characteristic_uuid](const bluetooth::mojom::CharacteristicInfoPtr&
                                  characteristic_info) {
          return characteristic_uuid ==
                 BluetoothUuidToNearbyUuid(characteristic_info->uuid);
        });
    if (char_it != characteristics.value().end()) {
      VLOG(1) << __func__ << " found characteristic";
      return std::make_pair(char_it->Clone(), gatt_service->service_info->id);
    }
  }

  LOG(WARNING) << __func__ << ": no match for characteristic at UUID"
               << std::string(characteristic_uuid);
  return std::nullopt;
}

void BleV2GattClient::Shutdown(base::WaitableEvent* shutdown_waitable_event) {
  CHECK(task_runner_->RunsTasksInCurrentSequence());

  // The `remote_device_` might have already been reset in `OnMojoDisconnect()`.
  if (remote_device_.is_bound()) {
    Disconnect();

    // Note that resetting the Remote will cancel any pending callbacks,
    // including those already in the task queue.
    remote_device_.reset();
  }

  // Note that resetting the Remote will cancel any pending callbacks, including
  // those already in the task queue.
  remote_device_.reset();
  uuid_to_discovered_gatt_services_map_.clear();

  // Cancel all pending calls. This is sequence safe because all
  // changes to the pending-event sets are sequenced. Make a copy of the events
  // because elements will be removed from the sets during iteration.
  CancelPendingTasks(pending_discover_services_waitable_events_);
  CancelPendingTasks(pending_get_characteristics_waitable_events_);
  CancelPendingTasks(pending_read_characteristic_waitable_events_);

  shutdown_waitable_event->Signal();
}

void BleV2GattClient::OnMojoDisconnect() {
  LOG(WARNING) << __func__ << ": Device remote unexpectedly disconnected";
  remote_device_.reset();
  uuid_to_discovered_gatt_services_map_.clear();
}

}  // namespace nearby::chrome