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

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

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

#include "base/containers/contains.h"
#include "base/metrics/histogram_functions.h"
#include "chrome/services/sharing/nearby/platform/bluetooth_device.h"

namespace nearby {
namespace chrome {

namespace {
// Client name for logging in BLE scanning.
constexpr char kScanClientName[] = "Nearby Connections";

void LogStartAdvertisingResult(bool success) {
  base::UmaHistogramBoolean(
      "Nearby.Connections.Bluetooth.LEMedium.StartAdvertising.Result", success);
}

void LogStopAdvertisingResult(bool success) {
  base::UmaHistogramBoolean(
      "Nearby.Connections.Bluetooth.LEMedium.StopAdvertising.Result", success);
}

void LogStartScanningResult(bool success) {
  base::UmaHistogramBoolean(
      "Nearby.Connections.Bluetooth.LEMedium.StartScanning.Result", success);
}

void LogStopScanningResult(bool success) {
  base::UmaHistogramBoolean(
      "Nearby.Connections.Bluetooth.LEMedium.StopScanning.Result", success);
}
}  // namespace

BleMedium::BleMedium(
    const mojo::SharedRemote<bluetooth::mojom::Adapter>& adapter)
    : adapter_(adapter) {
  DCHECK(adapter_.is_bound());
}

BleMedium::~BleMedium() {
  for (auto& it : registered_advertisements_map_) {
    // Note: this call is blocking.
    it.second->Unregister();
  }
}

bool BleMedium::StartAdvertising(
    const std::string& service_id,
    const ByteArray& advertisement,
    const std::string& fast_advertisement_service_uuid) {
  // Chrome Nearby BLE cannot support regular advertisements; only Fast
  // Advertisements. Ensure |fast_advertisement_service_uuid| is provided.
  DCHECK(!fast_advertisement_service_uuid.empty());

  StopAdvertising(service_id);

  auto service_uuid = device::BluetoothUUID(fast_advertisement_service_uuid);

  mojo::PendingRemote<bluetooth::mojom::Advertisement> pending_advertisement;
  bool success = adapter_->RegisterAdvertisement(
      service_uuid,
      std::vector<uint8_t>(advertisement.data(),
                           advertisement.data() + advertisement.size()),
      /*use_scan_data=*/true, /*connectable=*/false, &pending_advertisement);

  if (!success || !pending_advertisement.is_valid()) {
    LogStartAdvertisingResult(false);
    return false;
  }

  registered_service_id_to_fast_advertisement_service_uuid_map_.emplace(
      service_id, service_uuid);

  auto& remote_advertisement =
      registered_advertisements_map_
          .emplace(service_uuid, std::move(pending_advertisement))
          .first->second;
  remote_advertisement.set_disconnect_handler(base::BindOnce(
      &BleMedium::AdvertisementReleased, base::Unretained(this), service_uuid));

  LogStartAdvertisingResult(true);
  return true;
}

bool BleMedium::StopAdvertising(const std::string& service_id) {
  auto uuid_it =
      registered_service_id_to_fast_advertisement_service_uuid_map_.find(
          service_id);
  if (uuid_it ==
      registered_service_id_to_fast_advertisement_service_uuid_map_.end()) {
    LogStopAdvertisingResult(true);
    return true;
  }

  auto advertisement_it = registered_advertisements_map_.find(uuid_it->second);
  registered_service_id_to_fast_advertisement_service_uuid_map_.erase(uuid_it);

  if (advertisement_it == registered_advertisements_map_.end()) {
    LogStopAdvertisingResult(true);
    return true;
  }

  bool success = advertisement_it->second->Unregister();
  registered_advertisements_map_.erase(advertisement_it);

  LogStopAdvertisingResult(success);
  return success;
}

bool BleMedium::StartScanning(
    const std::string& service_id,
    const std::string& fast_advertisement_service_uuid,
    api::BleMedium::DiscoveredPeripheralCallback
        discovered_peripheral_callback) {
  auto service_uuid = device::BluetoothUUID(fast_advertisement_service_uuid);

  // The ID-to-UUID map should always be in sync with the callbacks map, and we
  // assume that the ID-UUID mapping is one-to-one.
  DCHECK_EQ(base::Contains(discovered_peripheral_callbacks_map_, service_uuid),
            base::Contains(
                discovery_service_id_to_fast_advertisement_service_uuid_map_,
                service_id));

  if (IsScanning() &&
      base::Contains(discovered_peripheral_callbacks_map_, service_uuid)) {
    LogStartScanningResult(true);
    return true;
  }

  // The ID-to-UUID map should always be in sync with the callbacks map.
  DCHECK_EQ(
      discovered_peripheral_callbacks_map_.empty(),
      discovery_service_id_to_fast_advertisement_service_uuid_map_.empty());

  // We only need to start discovery if no other discovery request is active.
  if (discovered_peripheral_callbacks_map_.empty()) {
    discovered_ble_peripherals_map_.clear();

    bool success =
        adapter_->AddObserver(adapter_observer_.BindNewPipeAndPassRemote());
    if (!success) {
      adapter_observer_.reset();
      LogStartScanningResult(false);
      return false;
    }

    mojo::PendingRemote<bluetooth::mojom::DiscoverySession> discovery_session;
    success =
        adapter_->StartDiscoverySession(kScanClientName, &discovery_session);

    if (!success || !discovery_session.is_valid()) {
      adapter_observer_.reset();
      LogStartScanningResult(false);
      return false;
    }

    discovery_session_.Bind(std::move(discovery_session));
    discovery_session_.set_disconnect_handler(
        base::BindOnce(&BleMedium::DiscoveringChanged, base::Unretained(this),
                       /*discovering=*/false));
  }

  // A different DiscoveredPeripheralCallback is being passed on each call, so
  // each must be captured and associated with its service UUID.
  discovered_peripheral_callbacks_map_.insert(
      {service_uuid, std::move(discovered_peripheral_callback)});

  discovery_service_id_to_fast_advertisement_service_uuid_map_.insert(
      {service_id, service_uuid});
  for (auto& uuid_peripheral_pair : discovered_ble_peripherals_map_) {
    uuid_peripheral_pair.second.UpdateIdToUuidMap(
        discovery_service_id_to_fast_advertisement_service_uuid_map_);
  }

  LogStartScanningResult(true);
  return true;
}

bool BleMedium::StopScanning(const std::string& service_id) {
  const auto it =
      discovery_service_id_to_fast_advertisement_service_uuid_map_.find(
          service_id);
  if (it !=
      discovery_service_id_to_fast_advertisement_service_uuid_map_.end()) {
    DCHECK(base::Contains(discovered_peripheral_callbacks_map_, it->second));
    discovered_peripheral_callbacks_map_.erase(it->second);
    discovery_service_id_to_fast_advertisement_service_uuid_map_.erase(it);
    for (auto& uuid_peripheral_pair : discovered_ble_peripherals_map_) {
      uuid_peripheral_pair.second.UpdateIdToUuidMap(
          discovery_service_id_to_fast_advertisement_service_uuid_map_);
    }
  }

  // The ID-to-UUID map should always be in sync with the callbacks map.
  DCHECK_EQ(
      discovered_peripheral_callbacks_map_.empty(),
      discovery_service_id_to_fast_advertisement_service_uuid_map_.empty());

  if (!discovered_peripheral_callbacks_map_.empty()) {
    LogStopScanningResult(true);
    return true;
  }

  bool stop_discovery_success = true;
  if (discovery_session_) {
    bool message_success = discovery_session_->Stop(&stop_discovery_success);
    stop_discovery_success = stop_discovery_success && message_success;
  }

  adapter_observer_.reset();
  discovery_session_.reset();

  LogStopScanningResult(stop_discovery_success);
  return stop_discovery_success;
}

bool BleMedium::StartAcceptingConnections(
    const std::string& service_id,
    api::BleMedium::AcceptedConnectionCallback accepted_connection_callback) {
  // Do not actually start a GATT server, because BLE connections are not yet
  // supported in Chrome Nearby. However, return true in order to allow
  // BLE advertising to continue.

  // TODO(hansberry): Verify if this is still required in NCv2.
  return true;
}

bool BleMedium::StopAcceptingConnections(const std::string& service_id) {
  // Do nothing. BLE connections are not yet supported in Chrome Nearby.
  return false;
}

std::unique_ptr<api::BleSocket> BleMedium::Connect(
    api::BlePeripheral& ble_peripheral,
    const std::string& service_id,
    CancellationFlag* cancellation_flag) {
  // Do nothing. BLE connections are not yet supported in Chrome Nearby.
  return nullptr;
}

void BleMedium::PresentChanged(bool present) {
  // TODO(hansberry): It is unclear to me how the API implementation can signal
  // to Core that |present| has become unexpectedly false. Need to ask
  // Nearby team.
  if (!present)
    StopScanning();
}

void BleMedium::PoweredChanged(bool powered) {
  // TODO(hansberry): It is unclear to me how the API implementation can signal
  // to Core that |powered| has become unexpectedly false. Need to ask
  // Nearby team.
  if (!powered)
    StopScanning();
}

void BleMedium::DiscoverableChanged(bool discoverable) {
  // Do nothing. BleMedium is not responsible for managing
  // discoverable state.
}

void BleMedium::DiscoveringChanged(bool discovering) {
  // TODO(hansberry): It is unclear to me how the API implementation can signal
  // to Core that |discovering| has become unexpectedly false. Need to ask
  // Nearby team.
  if (!discovering)
    StopScanning();
}

void BleMedium::DeviceAdded(bluetooth::mojom::DeviceInfoPtr device) {
  if (!IsScanning())
    return;

  // Best-effort attempt to filter out BT Classic devices. Dual-mode (BT
  // Classic and BLE) devices which the system has paired and/or connected to
  // may also expose service data, but all BLE advertisements that we are
  // interested in are captured in an element of |service_data_map|. See
  // BluetoothClassicMedium for separate discovery of BT Classic devices.
  if (device->service_data_map.empty())
    return;

  // Add a new or update the existing discovered peripheral. Note: Because
  // BlePeripherals are passed by reference to NearbyConnections, if a
  // BlePeripheral already exists with the given address, the reference should
  // not be invalidated, the update functions should be called instead.
  const std::string address = device->address;
  auto* existing_ble_peripheral = GetDiscoveredBlePeripheral(address);
  if (existing_ble_peripheral) {
    existing_ble_peripheral->UpdateDeviceInfo(std::move(device));
  } else {
    discovered_ble_peripherals_map_.emplace(
        address,
        chrome::BlePeripheral(
            std::move(device),
            discovery_service_id_to_fast_advertisement_service_uuid_map_));
  }

  // Copy the ID-to-UUID map to ensure that elements are not invalidated while
  // iterating--for example, if StopScanning() is triggered after invoking the
  // callback in the body of the loop.
  auto id_uuid_map_copy =
      discovery_service_id_to_fast_advertisement_service_uuid_map_;
  for (const auto& id_uuid_pair : id_uuid_map_copy) {
    // A callback should always be found unless an element was removed while we
    // were iterating through the IDs.
    const auto it =
        discovered_peripheral_callbacks_map_.find(id_uuid_pair.second);
    if (it == discovered_peripheral_callbacks_map_.end())
      continue;

    // Fetch the BlePeripheral with the same `address` again because
    // previously fetched pointers may have been invalidated while iterating
    // through the IDs.
    auto* ble_peripheral = GetDiscoveredBlePeripheral(address);
    if (!ble_peripheral)
      continue;

    // Do not perform any filtering here, for example, by checking if the
    // peripheral has non-empty advertisement bytes. Unconditionally inform all
    // callbacks of the discovered device, and rely on the Nearby Connections
    // library to perform the filtering.
    it->second.peripheral_discovered_cb(*ble_peripheral, id_uuid_pair.first,
                                        /*fast_advertisement=*/true);
  }
}

void BleMedium::DeviceChanged(bluetooth::mojom::DeviceInfoPtr device) {
  DeviceAdded(std::move(device));
}

void BleMedium::DeviceRemoved(bluetooth::mojom::DeviceInfoPtr device) {
  if (!IsScanning())
    return;

  const std::string& address = device->address;
  if (!GetDiscoveredBlePeripheral(address))
    return;

  // Copy the ID-to-UUID map to ensure that elements are not invalidated while
  // iterating--for example, if StopScanning() is triggered after invoking the
  // callback in the body of the loop.
  auto id_uuid_map_copy =
      discovery_service_id_to_fast_advertisement_service_uuid_map_;
  for (const auto& id_uuid_pair : id_uuid_map_copy) {
    // A callback should always be found unless an element was removed while we
    // were iterating through the IDs.
    const auto it =
        discovered_peripheral_callbacks_map_.find(id_uuid_pair.second);
    if (it == discovered_peripheral_callbacks_map_.end())
      continue;

    // Fetch |ble_peripheral| again because it might have since been invalidated
    // while we were iterating through IDs.
    auto* ble_peripheral = GetDiscoveredBlePeripheral(address);
    if (!ble_peripheral)
      continue;

    it->second.peripheral_lost_cb(*ble_peripheral, id_uuid_pair.first);
  }
}

void BleMedium::AdvertisementReleased(
    const device::BluetoothUUID& service_uuid) {
  registered_advertisements_map_.erase(service_uuid);
}

bool BleMedium::IsScanning() {
  DCHECK_EQ(
      discovered_peripheral_callbacks_map_.empty(),
      discovery_service_id_to_fast_advertisement_service_uuid_map_.empty());
  return adapter_observer_.is_bound() && discovery_session_.is_bound() &&
         !discovered_peripheral_callbacks_map_.empty();
}

void BleMedium::StopScanning() {
  // We cannot simply iterate over
  // |discovery_service_id_to_fast_advertisement_service_uuid_map_| because
  // StopScanning() will erase the provided element.
  while (
      !discovery_service_id_to_fast_advertisement_service_uuid_map_.empty()) {
    StopScanning(
        /*service_id=*/
        discovery_service_id_to_fast_advertisement_service_uuid_map_.begin()
            ->first);
  }
  DCHECK(discovered_peripheral_callbacks_map_.empty());
}

chrome::BlePeripheral* BleMedium::GetDiscoveredBlePeripheral(
    const std::string& address) {
  auto it = discovered_ble_peripherals_map_.find(address);
  return it == discovered_ble_peripherals_map_.end() ? nullptr : &it->second;
}

}  // namespace chrome
}  // namespace nearby