chromium/device/bluetooth/bluetooth_low_energy_advertisement_manager_mac.mm

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

#include "device/bluetooth/bluetooth_low_energy_advertisement_manager_mac.h"

#include <optional>

#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/strings/sys_string_conversions.h"
#import "base/task/single_thread_task_runner.h"
#include "device/bluetooth/bluetooth_advertisement.h"

namespace device {

BluetoothLowEnergyAdvertisementManagerMac::
    BluetoothLowEnergyAdvertisementManagerMac() = default;

BluetoothLowEnergyAdvertisementManagerMac::
    ~BluetoothLowEnergyAdvertisementManagerMac() = default;

void BluetoothLowEnergyAdvertisementManagerMac::Init(
    scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
    CBPeripheralManager* peripheral_manager) {
  ui_task_runner_ = ui_task_runner;
  peripheral_manager_ = peripheral_manager;
}

void BluetoothLowEnergyAdvertisementManagerMac::
    OnPeripheralManagerStateChanged() {
  // This function handles several cases:
  //   1. Error out when registering an advertisement, but the adapter is not
  //      powered on.
  //   2. Start advertising a registered advertisement if the adapter is powered
  //      on.
  //   3. Stop the advertisement when the adapter is powered off.
  //      Note that if the adapter is powered off while advertising, macOS will
  //      automatically restart advertising when the adapter is powered back on,
  //      so we need to explicitly stop advertising in this case.

  if (!active_advertisement_) {
    return;
  }

  CBManagerState adapter_state = [peripheral_manager_ state];
  if (adapter_state == CBManagerStateUnknown) {
    // Wait for the adapter to initialize.
    return;
  }

  if (active_advertisement_->is_waiting_for_adapter() &&
      adapter_state < CBManagerStatePoweredOn) {
    DVLOG(1)
        << "Registration failed. Adapter changed to invalid adapter_state.";
    BluetoothAdvertisement::ErrorCode error_code =
        adapter_state == CBManagerStatePoweredOff
            ? BluetoothAdvertisement::ERROR_ADAPTER_POWERED_OFF
            : BluetoothAdvertisement::ERROR_UNSUPPORTED_PLATFORM;
    active_advertisement_->OnAdvertisementError(ui_task_runner_.get(),
                                                error_code);
    active_advertisement_ = nullptr;
    return;
  }

  if (active_advertisement_->is_advertising() &&
      adapter_state == CBManagerStateResetting) {
    DVLOG(1) << "Adapter resetting. Invalidating advertisement.";
    active_advertisement_->OnAdapterReset();
    active_advertisement_ = nullptr;
    return;
  }

  if (active_advertisement_->is_advertising() &&
      adapter_state == CBManagerStatePoweredOff) {
    DVLOG(1) << "Adapter powered off. Stopping advertisement.";
    // Note: we purposefully don't unregister the active advertisement for
    // consistency with ChromeOS. The caller must manually unregister
    // the advertisement themselves.
    [peripheral_manager_ stopAdvertising];
    return;
  }

  if (active_advertisement_->is_waiting_for_adapter()) {
    StartAdvertising();
  }
}

void BluetoothLowEnergyAdvertisementManagerMac::StartAdvertising() {
  NSMutableArray* service_uuid_array = [[NSMutableArray alloc]
      initWithCapacity:active_advertisement_->service_uuids().size()];
  for (const std::string& service_uuid :
       active_advertisement_->service_uuids()) {
    NSString* uuid_string = base::SysUTF8ToNSString(service_uuid);
    [service_uuid_array addObject:[CBUUID UUIDWithString:uuid_string]];
  }

  active_advertisement_->OnAdvertisementPending();
  [peripheral_manager_ startAdvertising:@{
    CBAdvertisementDataServiceUUIDsKey : service_uuid_array
  }];
}

void BluetoothLowEnergyAdvertisementManagerMac::RegisterAdvertisement(
    std::unique_ptr<BluetoothAdvertisement::Data> advertisement_data,
    BluetoothAdapter::CreateAdvertisementCallback callback,
    BluetoothAdapter::AdvertisementErrorCallback error_callback) {
  std::optional<BluetoothAdvertisement::ErrorCode> error_code;

  const std::optional<BluetoothAdvertisement::UUIDList>& service_uuids =
      advertisement_data->service_uuids();
  if (!service_uuids || advertisement_data->manufacturer_data() ||
      advertisement_data->solicit_uuids() ||
      advertisement_data->service_data()) {
    DVLOG(1) << "macOS only supports advertising service UUIDs.";
    error_code = BluetoothAdvertisement::ERROR_UNSUPPORTED_PLATFORM;
  }

  if (active_advertisement_ && active_advertisement_->status() !=
                                   BluetoothAdvertisementMac::UNREGISTERED) {
    DVLOG(1) << "Only one active BLE advertisement is currently supported.";
    error_code = BluetoothAdvertisement::ERROR_ADVERTISEMENT_ALREADY_EXISTS;
  }

  if (error_code) {
    ui_task_runner_->PostTask(
        FROM_HERE, base::BindOnce(std::move(error_callback), *error_code));
    return;
  }

  active_advertisement_ = base::MakeRefCounted<BluetoothAdvertisementMac>(
      std::move(service_uuids), std::move(callback), std::move(error_callback),
      this);
  OnPeripheralManagerStateChanged();
}

void BluetoothLowEnergyAdvertisementManagerMac::UnregisterAdvertisement(
    BluetoothAdvertisementMac* advertisement,
    BluetoothAdvertisement::SuccessCallback success_callback,
    BluetoothAdvertisement::ErrorCallback error_callback) {
  if (advertisement != active_advertisement_.get()) {
    DVLOG(1) << "Cannot unregister none-active advertisement.";
    ui_task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(error_callback),
                       BluetoothAdvertisement::ERROR_RESET_ADVERTISING));
    return;
  }

  active_advertisement_ = nullptr;
  [peripheral_manager_ stopAdvertising];
  ui_task_runner_->PostTask(FROM_HERE, std::move(success_callback));
}

void BluetoothLowEnergyAdvertisementManagerMac::DidStartAdvertising(
    NSError* error) {
  DCHECK(active_advertisement_ &&
         active_advertisement_->is_advertisement_pending());
  if (!active_advertisement_ ||
      !active_advertisement_->is_advertisement_pending()) {
    return;
  }

  if (error != nil) {
    DVLOG(1) << "Error advertising: "
             << base::SysNSStringToUTF8(error.localizedDescription);
    active_advertisement_->OnAdvertisementError(
        ui_task_runner_.get(),
        BluetoothAdvertisement::ERROR_STARTING_ADVERTISEMENT);
    active_advertisement_ = nullptr;
    return;
  }

  active_advertisement_->OnAdvertisementSuccess(ui_task_runner_.get());
}

}  // device