blob: 8d9df355abc1e044f73e4d5670c54cad113d064d [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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_device_mac.h"
#import <CoreFoundation/CoreFoundation.h>
#include <stddef.h>
#include "base/mac/mac_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/mac/sdk_forward_declarations.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "device/bluetooth/bluetooth_adapter_mac.h"
#include "device/bluetooth/bluetooth_adapter_mac_metrics.h"
#include "device/bluetooth/bluetooth_device.h"
#include "device/bluetooth/bluetooth_low_energy_peripheral_delegate.h"
#include "device/bluetooth/bluetooth_remote_gatt_characteristic_mac.h"
#include "device/bluetooth/bluetooth_remote_gatt_descriptor_mac.h"
#include "device/bluetooth/bluetooth_remote_gatt_service_mac.h"
// Remove when Chrome no longer supports 10.12.
#if defined(MAC_OS_X_VERSION_10_13)
// In the 10.13 SDK, CBPeripheral became a subclass of CBPeer, which defines
// -[CBPeer identifier] as partially available. Pretend it still exists on
// CBPeripheral. At runtime the implementation on CBPeer will be invoked.
@interface CBPeripheral (HighSierraSDK)
@property(readonly, nonatomic) NSUUID* identifier;
@end
#endif // MAC_OS_X_VERSION_10_13
namespace device {
BluetoothLowEnergyDeviceMac::BluetoothLowEnergyDeviceMac(
BluetoothAdapterMac* adapter,
CBPeripheral* peripheral)
: BluetoothDeviceMac(adapter),
peripheral_(peripheral, base::scoped_policy::RETAIN),
connected_(false),
discovery_pending_count_(0) {
DCHECK(BluetoothAdapterMac::IsLowEnergyAvailable());
DCHECK(peripheral_);
peripheral_delegate_.reset([[BluetoothLowEnergyPeripheralDelegate alloc]
initWithBluetoothLowEnergyDeviceMac:this]);
[peripheral_ setDelegate:peripheral_delegate_];
identifier_ = GetPeripheralIdentifier(peripheral);
hash_address_ = GetPeripheralHashAddress(peripheral);
UpdateTimestamp();
}
BluetoothLowEnergyDeviceMac::~BluetoothLowEnergyDeviceMac() {
if (IsGattConnected()) {
GetMacAdapter()->DisconnectGatt(this);
}
[peripheral_ setDelegate:nil];
}
std::string BluetoothLowEnergyDeviceMac::GetIdentifier() const {
return identifier_;
}
uint32_t BluetoothLowEnergyDeviceMac::GetBluetoothClass() const {
return 0x1F00; // Unspecified Device Class
}
std::string BluetoothLowEnergyDeviceMac::GetAddress() const {
return hash_address_;
}
BluetoothDevice::VendorIDSource BluetoothLowEnergyDeviceMac::GetVendorIDSource()
const {
return VENDOR_ID_UNKNOWN;
}
uint16_t BluetoothLowEnergyDeviceMac::GetVendorID() const {
return 0;
}
uint16_t BluetoothLowEnergyDeviceMac::GetProductID() const {
return 0;
}
uint16_t BluetoothLowEnergyDeviceMac::GetDeviceID() const {
return 0;
}
uint16_t BluetoothLowEnergyDeviceMac::GetAppearance() const {
// TODO(crbug.com/588083): Implementing GetAppearance()
// on mac, win, and android platforms for chrome
NOTIMPLEMENTED();
return 0;
}
base::Optional<std::string> BluetoothLowEnergyDeviceMac::GetName() const {
if ([peripheral_ name])
return base::SysNSStringToUTF8([peripheral_ name]);
return base::nullopt;
}
bool BluetoothLowEnergyDeviceMac::IsPaired() const {
return GetMacAdapter()->IsBluetoothLowEnergyDeviceSystemPaired(identifier_);
}
bool BluetoothLowEnergyDeviceMac::IsConnected() const {
return IsGattConnected();
}
bool BluetoothLowEnergyDeviceMac::IsGattConnected() const {
// |connected_| can be false while |[peripheral_ state]| is
// |CBPeripheralStateConnected|. This happens
// BluetoothAdapterMac::DidConnectPeripheral() is called and
// BluetoothLowEnergyDeviceMac::DidConnectGatt() has not been called yet.
return connected_;
}
bool BluetoothLowEnergyDeviceMac::IsConnectable() const {
// Only available for Chrome OS.
NOTIMPLEMENTED();
return false;
}
bool BluetoothLowEnergyDeviceMac::IsConnecting() const {
return ([peripheral_ state] == CBPeripheralStateConnecting);
}
bool BluetoothLowEnergyDeviceMac::ExpectingPinCode() const {
return false;
}
bool BluetoothLowEnergyDeviceMac::ExpectingPasskey() const {
return false;
}
bool BluetoothLowEnergyDeviceMac::ExpectingConfirmation() const {
return false;
}
void BluetoothLowEnergyDeviceMac::GetConnectionInfo(
const ConnectionInfoCallback& callback) {
NOTIMPLEMENTED();
}
void BluetoothLowEnergyDeviceMac::SetConnectionLatency(
ConnectionLatency connection_latency,
const base::Closure& callback,
const ErrorCallback& error_callback) {
NOTIMPLEMENTED();
}
void BluetoothLowEnergyDeviceMac::Connect(
PairingDelegate* pairing_delegate,
const base::Closure& callback,
const ConnectErrorCallback& error_callback) {
NOTIMPLEMENTED();
}
void BluetoothLowEnergyDeviceMac::SetPinCode(const std::string& pincode) {
NOTIMPLEMENTED();
}
void BluetoothLowEnergyDeviceMac::SetPasskey(uint32_t passkey) {
NOTIMPLEMENTED();
}
void BluetoothLowEnergyDeviceMac::ConfirmPairing() {
NOTIMPLEMENTED();
}
void BluetoothLowEnergyDeviceMac::RejectPairing() {
NOTIMPLEMENTED();
}
void BluetoothLowEnergyDeviceMac::CancelPairing() {
NOTIMPLEMENTED();
}
void BluetoothLowEnergyDeviceMac::Disconnect(
const base::Closure& callback,
const ErrorCallback& error_callback) {
NOTIMPLEMENTED();
}
void BluetoothLowEnergyDeviceMac::Forget(const base::Closure& callback,
const ErrorCallback& error_callback) {
NOTIMPLEMENTED();
}
void BluetoothLowEnergyDeviceMac::ConnectToService(
const BluetoothUUID& uuid,
const ConnectToServiceCallback& callback,
const ConnectToServiceErrorCallback& error_callback) {
NOTIMPLEMENTED();
}
void BluetoothLowEnergyDeviceMac::ConnectToServiceInsecurely(
const BluetoothUUID& uuid,
const ConnectToServiceCallback& callback,
const ConnectToServiceErrorCallback& error_callback) {
NOTIMPLEMENTED();
}
void BluetoothLowEnergyDeviceMac::CreateGattConnectionImpl() {
if (!IsGattConnected()) {
GetMacAdapter()->CreateGattConnection(this);
}
}
void BluetoothLowEnergyDeviceMac::DisconnectGatt() {
GetMacAdapter()->DisconnectGatt(this);
}
void BluetoothLowEnergyDeviceMac::DidDiscoverPrimaryServices(NSError* error) {
--discovery_pending_count_;
if (discovery_pending_count_ < 0) {
// This should never happen, just in case it happens with a device,
// discovery_pending_count_ is set back to 0.
VLOG(1) << *this
<< ": BluetoothLowEnergyDeviceMac::discovery_pending_count_ "
<< discovery_pending_count_;
discovery_pending_count_ = 0;
return;
}
RecordDidDiscoverPrimaryServicesResult(error);
if (error) {
// TODO(http://crbug.com/609320): Need to pass the error.
// TODO(http://crbug.com/609844): Decide what to do if discover failed
// a device services.
VLOG(1) << *this << ": Can't discover primary services: "
<< BluetoothAdapterMac::String(error);
return;
}
if (!IsGattConnected()) {
// Don't create services if the device disconnected.
VLOG(1) << *this << ": DidDiscoverPrimaryServices, gatt not connected.";
return;
}
VLOG(1) << *this << ": DidDiscoverPrimaryServices, pending count: "
<< discovery_pending_count_;
for (CBService* cb_service in GetPeripheral().services) {
BluetoothRemoteGattServiceMac* gatt_service =
GetBluetoothRemoteGattServiceMac(cb_service);
if (!gatt_service) {
gatt_service = new BluetoothRemoteGattServiceMac(this, cb_service,
true /* is_primary */);
auto result_iter = gatt_services_.insert(std::make_pair(
gatt_service->GetIdentifier(), base::WrapUnique(gatt_service)));
DCHECK(result_iter.second);
VLOG(1) << *gatt_service << ": New service.";
adapter_->NotifyGattServiceAdded(gatt_service);
} else {
VLOG(1) << *gatt_service << ": Known service.";
}
}
if (discovery_pending_count_ == 0) {
for (auto it = gatt_services_.begin(); it != gatt_services_.end(); ++it) {
BluetoothRemoteGattService* gatt_service = it->second.get();
BluetoothRemoteGattServiceMac* gatt_service_mac =
static_cast<BluetoothRemoteGattServiceMac*>(gatt_service);
gatt_service_mac->DiscoverCharacteristics();
}
SendNotificationIfDiscoveryComplete();
}
}
void BluetoothLowEnergyDeviceMac::DidDiscoverCharacteristics(
CBService* cb_service,
NSError* error) {
RecordDidDiscoverCharacteristicsResult(error);
if (error) {
// TODO(http://crbug.com/609320): Need to pass the error.
// TODO(http://crbug.com/609844): Decide what to do if discover failed
VLOG(1) << *this << ": Can't discover characteristics: "
<< BluetoothAdapterMac::String(error);
return;
}
if (!IsGattConnected()) {
VLOG(1) << *this << ": DidDiscoverCharacteristics, gatt disconnected.";
// Don't create characteristics if the device disconnected.
return;
}
if (IsGattServicesDiscoveryComplete()) {
// This should never happen, just in case it happens with a device, this
// notification should be ignored.
VLOG(1) << *this
<< ": Discovery complete, ignoring DidDiscoverCharacteristics.";
return;
}
BluetoothRemoteGattServiceMac* gatt_service =
GetBluetoothRemoteGattServiceMac(cb_service);
DCHECK(gatt_service);
gatt_service->DidDiscoverCharacteristics();
SendNotificationIfDiscoveryComplete();
}
void BluetoothLowEnergyDeviceMac::DidModifyServices(
NSArray* invalidatedServices) {
VLOG(1) << *this << ": DidModifyServices: "
<< " invalidated services "
<< base::SysNSStringToUTF8([invalidatedServices description]);
for (CBService* cb_service in invalidatedServices) {
BluetoothRemoteGattServiceMac* gatt_service =
GetBluetoothRemoteGattServiceMac(cb_service);
DCHECK(gatt_service);
VLOG(1) << gatt_service->GetUUID().canonical_value();
std::unique_ptr<BluetoothRemoteGattService> scoped_service =
std::move(gatt_services_[gatt_service->GetIdentifier()]);
gatt_services_.erase(gatt_service->GetIdentifier());
adapter_->NotifyGattServiceRemoved(scoped_service.get());
}
device_uuids_.ClearServiceUUIDs();
SetGattServicesDiscoveryComplete(false);
adapter_->NotifyDeviceChanged(this);
DiscoverPrimaryServices();
}
void BluetoothLowEnergyDeviceMac::DidUpdateValue(
CBCharacteristic* characteristic,
NSError* error) {
BluetoothRemoteGattCharacteristicMac* gatt_characteristic_mac =
GetBluetoothRemoteGattCharacteristicMac(characteristic);
DCHECK(gatt_characteristic_mac);
gatt_characteristic_mac->DidUpdateValue(error);
}
void BluetoothLowEnergyDeviceMac::DidWriteValue(
CBCharacteristic* characteristic,
NSError* error) {
BluetoothRemoteGattCharacteristicMac* gatt_characteristic_mac =
GetBluetoothRemoteGattCharacteristicMac(characteristic);
DCHECK(gatt_characteristic_mac);
gatt_characteristic_mac->DidWriteValue(error);
}
void BluetoothLowEnergyDeviceMac::DidUpdateNotificationState(
CBCharacteristic* characteristic,
NSError* error) {
BluetoothRemoteGattCharacteristicMac* gatt_characteristic_mac =
GetBluetoothRemoteGattCharacteristicMac(characteristic);
DCHECK(gatt_characteristic_mac);
gatt_characteristic_mac->DidUpdateNotificationState(error);
}
void BluetoothLowEnergyDeviceMac::DidDiscoverDescriptors(
CBCharacteristic* cb_characteristic,
NSError* error) {
RecordDidDiscoverDescriptorsResult(error);
if (error) {
// TODO(http://crbug.com/609320): Need to pass the error.
// TODO(http://crbug.com/609844): Decide what to do if discover failed
VLOG(1) << *this << ": Can't discover descriptors: "
<< BluetoothAdapterMac::String(error);
return;
}
if (!IsGattConnected()) {
VLOG(1) << *this << ": DidDiscoverDescriptors, disconnected.";
// Don't discover descriptors if the device disconnected.
return;
}
if (IsGattServicesDiscoveryComplete()) {
// This should never happen, just in case it happens with a device, this
// notification should be ignored.
VLOG(1) << *this
<< ": Discovery complete, ignoring DidDiscoverDescriptors.";
return;
}
BluetoothRemoteGattServiceMac* gatt_service =
GetBluetoothRemoteGattServiceMac(cb_characteristic.service);
DCHECK(gatt_service);
gatt_service->DidDiscoverDescriptors(cb_characteristic);
SendNotificationIfDiscoveryComplete();
}
void BluetoothLowEnergyDeviceMac::DidUpdateValueForDescriptor(
CBDescriptor* cb_descriptor,
NSError* error) {
BluetoothRemoteGattDescriptorMac* gatt_descriptor =
GetBluetoothRemoteGattDescriptorMac(cb_descriptor);
DCHECK(gatt_descriptor);
gatt_descriptor->DidUpdateValueForDescriptor(error);
}
void BluetoothLowEnergyDeviceMac::DidWriteValueForDescriptor(
CBDescriptor* cb_descriptor,
NSError* error) {
BluetoothRemoteGattDescriptorMac* gatt_descriptor =
GetBluetoothRemoteGattDescriptorMac(cb_descriptor);
DCHECK(gatt_descriptor);
gatt_descriptor->DidWriteValueForDescriptor(error);
}
// static
std::string BluetoothLowEnergyDeviceMac::GetPeripheralIdentifier(
CBPeripheral* peripheral) {
DCHECK(BluetoothAdapterMac::IsLowEnergyAvailable());
NSUUID* uuid = [peripheral identifier];
NSString* uuidString = [uuid UUIDString];
return base::SysNSStringToUTF8(uuidString);
}
// static
std::string BluetoothLowEnergyDeviceMac::GetPeripheralHashAddress(
CBPeripheral* peripheral) {
return GetPeripheralHashAddress(GetPeripheralIdentifier(peripheral));
}
// static
std::string BluetoothLowEnergyDeviceMac::GetPeripheralHashAddress(
base::StringPiece device_identifier) {
const size_t kCanonicalAddressNumberOfBytes = 6;
char raw[kCanonicalAddressNumberOfBytes];
crypto::SHA256HashString(device_identifier, raw, sizeof(raw));
return BluetoothDevice::CanonicalizeAddress(
base::HexEncode(raw, sizeof(raw)));
}
void BluetoothLowEnergyDeviceMac::DidConnectPeripheral() {
VLOG(1) << *this << ": GATT connected.";
if (!connected_) {
connected_ = true;
DidConnectGatt();
DiscoverPrimaryServices();
} else {
// -[<CBCentralManagerDelegate> centralManager:didConnectPeripheral:] can be
// called twice because of a macOS bug. This second call should be ignored.
// See crbug.com/681414.
VLOG(1) << *this << ": Already connected, ignoring event.";
}
}
void BluetoothLowEnergyDeviceMac::DiscoverPrimaryServices() {
VLOG(1) << *this << ": DiscoverPrimaryServices, pending count "
<< discovery_pending_count_;
++discovery_pending_count_;
[GetPeripheral() discoverServices:nil];
}
void BluetoothLowEnergyDeviceMac::SendNotificationIfDiscoveryComplete() {
DCHECK(!IsGattServicesDiscoveryComplete());
// Notify when all services have been discovered.
bool discovery_complete =
discovery_pending_count_ == 0 &&
std::find_if_not(
gatt_services_.begin(),
gatt_services_.end(), [](GattServiceMap::value_type & pair) {
BluetoothRemoteGattService* gatt_service = pair.second.get();
return static_cast<BluetoothRemoteGattServiceMac*>(gatt_service)
->IsDiscoveryComplete();
}) == gatt_services_.end();
if (discovery_complete) {
VLOG(1) << *this << ": Discovery complete.";
device_uuids_.ReplaceServiceUUIDs(gatt_services_);
SetGattServicesDiscoveryComplete(true);
adapter_->NotifyGattServicesDiscovered(this);
adapter_->NotifyDeviceChanged(this);
}
}
BluetoothAdapterMac* BluetoothLowEnergyDeviceMac::GetMacAdapter() {
return static_cast<BluetoothAdapterMac*>(this->adapter_);
}
BluetoothAdapterMac* BluetoothLowEnergyDeviceMac::GetMacAdapter() const {
return static_cast<BluetoothAdapterMac*>(this->adapter_);
}
CBPeripheral* BluetoothLowEnergyDeviceMac::GetPeripheral() {
return peripheral_;
}
BluetoothRemoteGattServiceMac*
BluetoothLowEnergyDeviceMac::GetBluetoothRemoteGattServiceMac(
CBService* cb_service) const {
for (auto it = gatt_services_.begin(); it != gatt_services_.end(); ++it) {
BluetoothRemoteGattService* gatt_service = it->second.get();
BluetoothRemoteGattServiceMac* gatt_service_mac =
static_cast<BluetoothRemoteGattServiceMac*>(gatt_service);
if (gatt_service_mac->GetService() == cb_service)
return gatt_service_mac;
}
return nullptr;
}
BluetoothRemoteGattCharacteristicMac*
BluetoothLowEnergyDeviceMac::GetBluetoothRemoteGattCharacteristicMac(
CBCharacteristic* cb_characteristic) const {
CBService* cb_service = [cb_characteristic service];
BluetoothRemoteGattServiceMac* gatt_service_mac =
GetBluetoothRemoteGattServiceMac(cb_service);
if (!gatt_service_mac) {
return nullptr;
}
return gatt_service_mac->GetBluetoothRemoteGattCharacteristicMac(
cb_characteristic);
}
BluetoothRemoteGattDescriptorMac*
BluetoothLowEnergyDeviceMac::GetBluetoothRemoteGattDescriptorMac(
CBDescriptor* cb_descriptor) const {
CBService* cb_service = [[cb_descriptor characteristic] service];
BluetoothRemoteGattServiceMac* gatt_service_mac =
GetBluetoothRemoteGattServiceMac(cb_service);
if (!gatt_service_mac) {
return nullptr;
}
return gatt_service_mac->GetBluetoothRemoteGattDescriptorMac(cb_descriptor);
}
void BluetoothLowEnergyDeviceMac::DidDisconnectPeripheral(NSError* error) {
connected_ = false;
VLOG(1) << *this << ": Disconnected from peripheral.";
RecordDidDisconnectPeripheralResult(error);
if (error) {
VLOG(1) << *this
<< ": Bluetooth error: " << BluetoothAdapterMac::String(error);
}
SetGattServicesDiscoveryComplete(false);
// Removing all services at once to ensure that calling GetGattService on
// removed service in GattServiceRemoved returns null.
GattServiceMap gatt_services_swapped;
gatt_services_swapped.swap(gatt_services_);
gatt_services_swapped.clear();
device_uuids_.ClearServiceUUIDs();
// There are two cases in which this function will be called:
// 1. When the connection to the device breaks (either because
// we closed it or the device closed it).
// 2. When we cancel a pending connection request.
if (create_gatt_connection_error_callbacks_.empty()) {
// If there are no pending callbacks then the connection broke (#1).
DidDisconnectGatt();
return;
}
// Else we canceled the connection request (#2).
// TODO(http://crbug.com/585897): Need to pass the error.
DidFailToConnectGatt(BluetoothDevice::ConnectErrorCode::ERROR_FAILED);
}
std::ostream& operator<<(std::ostream& out,
const BluetoothLowEnergyDeviceMac& device) {
// TODO(crbug.com/703878): Should use
// BluetoothLowEnergyDeviceMac::GetNameForDisplay() instead.
base::Optional<std::string> name = device.GetName();
const char* is_gatt_connected =
device.IsGattConnected() ? "GATT connected" : "GATT disconnected";
return out << "<BluetoothLowEnergyDeviceMac " << device.GetAddress() << "/"
<< &device << ", " << is_gatt_connected << ", \""
<< name.value_or("Unnamed device") << "\">";
}
} // namespace device