IOS-CoreBluetooth-Mock/CoreBluetoothMock/CBMCentralManagerNative.swift

590 lines
24 KiB
Swift

/*
* Copyright (c) 2020, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this
* list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
import CoreBluetooth
/// An implementation of ``CBMCentralManager`` that will proxy all the requests and responses
/// to the native `CBCentralManager` object.
///
/// This manager can only interact with physical Bluetooth LE devices.
public class CBMCentralManagerNative: CBMCentralManager {
private var manager: CBCentralManager!
private var wrapper: CBCentralManagerDelegate!
private var peripherals: [UUID : CBMPeripheralNative] = [:]
private class CBMCentralManagerDelegateWrapper: NSObject, CBCentralManagerDelegate {
fileprivate weak var manager: CBMCentralManagerNative! // weak to avoid cyclic reference (#9)
init(_ manager: CBMCentralManagerNative) {
self.manager = manager
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
manager.delegate?.centralManagerDidUpdateState(manager)
}
// This methods is moved to a separate class below. Otherwise a warning
// is generated when setting delegate to the CBCentralManager when
// restoration was not enabled.
//
// func centralManager(_ central: CBCentralManager,
// willRestoreState dict: [String : Any]) {
// manager.delegate?.centralManager(manager, willRestoreState: dict)
// }
func centralManager(_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String : Any],
rssi RSSI: NSNumber) {
manager.delegate?.centralManager(manager,
didDiscover: getPeripheral(peripheral),
advertisementData: advertisementData,
rssi: RSSI)
}
func centralManager(_ central: CBCentralManager,
didConnect peripheral: CBPeripheral) {
manager.delegate?.centralManager(manager,
didConnect: getPeripheral(peripheral))
}
func centralManager(_ central: CBCentralManager,
didFailToConnect peripheral: CBPeripheral,
error: Error?) {
manager.delegate?.centralManager(manager,
didFailToConnect: getPeripheral(peripheral),
error: error)
}
func centralManager(_ central: CBCentralManager,
didDisconnectPeripheral peripheral: CBPeripheral,
error: Error?) {
manager.delegate?.centralManager(manager,
didDisconnectPeripheral: getPeripheral(peripheral),
error: error)
}
#if !os(macOS)
@available(iOS 13.0, *)
func centralManager(_ central: CBCentralManager,
didUpdateANCSAuthorizationFor peripheral: CBPeripheral) {
manager.delegate?.centralManager(manager,
didUpdateANCSAuthorizationFor: getPeripheral(peripheral))
}
#endif
#if !os(macOS)
@available(iOS 13.0, *)
func centralManager(_ central: CBCentralManager,
connectionEventDidOccur event: CBConnectionEvent,
for peripheral: CBPeripheral) {
manager.delegate?.centralManager(manager,
connectionEventDidOccur: event,
for: getPeripheral(peripheral))
}
#endif
private func getPeripheral(_ peripheral: CBPeripheral) -> CBMPeripheralNative {
return manager.peripherals[peripheral.identifier] ?? newPeripheral(peripheral)
}
private func newPeripheral(_ peripheral: CBPeripheral) -> CBMPeripheralNative {
let p = CBMPeripheralNative(peripheral)
manager.peripherals[peripheral.identifier] = p
return p
}
}
private class CBMCentralManagerDelegateWrapperWithRestoration: CBMCentralManagerDelegateWrapper {
override init(_ manager: CBMCentralManagerNative) {
super.init(manager)
}
func centralManager(_ central: CBCentralManager,
willRestoreState dict: [String : Any]) {
manager.delegate?.centralManager(manager, willRestoreState: dict)
}
}
public override var state: CBMManagerState {
return CBMManagerState(rawValue: manager.state.rawValue) ?? .unknown
}
@available(iOS 9.0, *)
public override var isScanning: Bool {
return manager.isScanning
}
@available(iOS, introduced: 13.0, deprecated: 13.1)
@available(macOS, introduced: 10.15)
@available(tvOS, introduced: 13.0, deprecated: 13.1)
@available(watchOS, introduced: 6.0, deprecated: 6.1)
public override var authorization: CBMManagerAuthorization {
return manager.authorization
}
@available(iOS 13.1, macOS 10.15, tvOS 13.1, watchOS 6.1, *)
public override class var authorization: CBMManagerAuthorization {
return CBCentralManager.authorization
}
#if !os(macOS)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public override class func supports(_ features: CBMCentralManager.Feature) -> Bool {
return CBCentralManager.supports(features)
}
#endif
public init() {
super.init(true)
self.wrapper = CBMCentralManagerDelegateWrapper(self)
self.manager = CBCentralManager()
self.manager.delegate = wrapper
}
public init(delegate: CBMCentralManagerDelegate?,
queue: DispatchQueue?) {
super.init(true)
self.wrapper = CBMCentralManagerDelegateWrapper(self)
self.manager = CBCentralManager(delegate: wrapper, queue: queue)
self.delegate = delegate
}
@available(iOS 7.0, *)
public init(delegate: CBMCentralManagerDelegate?,
queue: DispatchQueue?,
options: [String : Any]?) {
super.init(true)
let restoration = options?[CBMCentralManagerOptionRestoreIdentifierKey] != nil
self.wrapper = restoration ?
CBMCentralManagerDelegateWrapperWithRestoration(self) :
CBMCentralManagerDelegateWrapper(self)
self.manager = CBCentralManager(delegate: wrapper, queue: queue, options: options)
self.delegate = delegate
}
public override func scanForPeripherals(withServices serviceUUIDs: [CBMUUID]?,
options: [String : Any]? = nil) {
manager.scanForPeripherals(withServices: serviceUUIDs, options: options)
}
public override func stopScan() {
manager.stopScan()
}
public override func connect(_ peripheral: CBMPeripheral, options: [String : Any]?) {
if let peripheral = peripherals[peripheral.identifier] {
manager.connect(peripheral.peripheral, options: options)
}
}
public override func cancelPeripheralConnection(_ peripheral: CBMPeripheral) {
if let peripheral = peripherals[peripheral.identifier] {
manager.cancelPeripheralConnection(peripheral.peripheral)
}
}
@available(iOS 7.0, *)
public override func retrievePeripherals(withIdentifiers identifiers: [UUID]) -> [CBMPeripheral] {
let retrievedPeripherals = manager.retrievePeripherals(withIdentifiers: identifiers)
retrievedPeripherals
.forEach { peripherals[$0.identifier] = CBMPeripheralNative($0) }
return peripherals
.filter { identifiers.contains($0.key) }
.map { $0.value }
}
@available(iOS 7.0, *)
public override func retrieveConnectedPeripherals(withServices serviceUUIDs: [CBMUUID]) -> [CBMPeripheral] {
let retrievedPeripherals = manager.retrieveConnectedPeripherals(withServices: serviceUUIDs)
retrievedPeripherals
.forEach { peripherals[$0.identifier] = CBMPeripheralNative($0) }
return peripherals
.filter { entry in retrievedPeripherals.contains(where: { $0.identifier == entry.key }) }
.map { $0.value }
}
#if !os(macOS)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public override func registerForConnectionEvents(options: [CBMConnectionEventMatchingOption : Any]? = nil) {
manager.registerForConnectionEvents(options: options)
}
#endif
}
/// A native implementation of ``CBMPeripheral`` that will proxy all requests to an underlying `CBPeripheral`.
///
/// This implementation will be used when creating peripherals by ``CBMCentralManagerNative``.
///
/// Unless required, this class should not be accessed directly, but rather by the common protocol ``CBMPeripheral``.
public class CBMPeripheralNative: CBMPeer, CBMPeripheral {
private class CBPeripheralDelegateWrapper: NSObject, CBPeripheralDelegate {
private var impl: CBMPeripheralNative
init(_ peripheral: CBMPeripheralNative) {
self.impl = peripheral
}
func peripheralDidUpdateName(_ peripheral: CBPeripheral) {
impl.delegate?.peripheralDidUpdateName(impl)
}
func peripheral(_ peripheral: CBPeripheral,
didDiscoverServices error: Error?) {
smartCopy(peripheral.services)
impl.delegate?.peripheral(impl, didDiscoverServices: error)
}
func peripheral(_ peripheral: CBPeripheral,
didDiscoverIncludedServicesFor service: CBService,
error: Error?) {
smartCopy(peripheral.services)
usingMock(of: service) { peripheral, delegate, service in
delegate.peripheral(peripheral,
didDiscoverIncludedServicesFor: service,
error: error)
}
}
func peripheral(_ peripheral: CBPeripheral,
didDiscoverCharacteristicsFor service: CBService,
error: Error?) {
smartCopy(peripheral.services)
usingMock(of: service) { peripheral, delegate, service in
delegate.peripheral(peripheral,
didDiscoverCharacteristicsFor: service,
error: error)
}
}
func peripheral(_ peripheral: CBPeripheral,
didDiscoverDescriptorsFor characteristic: CBCharacteristic,
error: Error?) {
smartCopy(peripheral.services)
usingMock(of: characteristic) { peripheral, delegate, characteristic in
delegate.peripheral(peripheral,
didDiscoverDescriptorsFor: characteristic,
error: error)
}
}
@available(iOS 7.0, *)
func peripheral(_ peripheral: CBPeripheral,
didModifyServices invalidatedServices: [CBService]) {
var invalidatedServiceMocks: [CBMService] = []
invalidatedServices.forEach { service in
if let services = impl.mockServices,
let index = services
.firstIndex(where: { $0.service == service}) {
invalidatedServiceMocks.append(services[index])
impl.mockServices?.remove(at: index)
}
}
impl.delegate?.peripheral(impl, didModifyServices: invalidatedServiceMocks)
}
func peripheral(_ peripheral: CBPeripheral,
didUpdateNotificationStateFor characteristic: CBCharacteristic,
error: Error?) {
usingMock(of: characteristic) { peripheral, delegate, mock in
mock.isNotifying = characteristic.isNotifying
delegate.peripheral(peripheral,
didUpdateNotificationStateFor: mock,
error: error)
}
}
func peripheral(_ peripheral: CBPeripheral,
didWriteValueFor characteristic: CBCharacteristic,
error: Error?) {
usingMock(of: characteristic) { peripheral, delegate, characteristic in
delegate.peripheral(peripheral,
didWriteValueFor: characteristic,
error: error)
}
}
func peripheral(_ peripheral: CBPeripheral,
didWriteValueFor descriptor: CBDescriptor,
error: Error?) {
usingMock(of: descriptor) { peripheral, delegate, descriptor in
delegate.peripheral(peripheral,
didWriteValueFor: descriptor,
error: error)
}
}
func peripheral(_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?) {
usingMock(of: characteristic) { peripheral, delegate, mockCharacteristic in
mockCharacteristic.value = characteristic.value
delegate.peripheral(peripheral,
didUpdateValueFor: mockCharacteristic,
error: error)
}
}
func peripheral(_ peripheral: CBPeripheral,
didUpdateValueFor descriptor: CBDescriptor,
error: Error?) {
usingMock(of: descriptor) { peripheral, delegate, mockDescriptor in
mockDescriptor.value = descriptor.value
delegate.peripheral(peripheral,
didUpdateValueFor: mockDescriptor,
error: error)
}
}
func peripheral(_ peripheral: CBPeripheral,
didReadRSSI RSSI: NSNumber,
error: Error?) {
impl.delegate?.peripheral(impl, didReadRSSI: RSSI, error: error)
}
func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
if #available(iOS 11.0, tvOS 11.0, watchOS 4.0, *) {
impl.delegate?.peripheralIsReady(toSendWriteWithoutResponse: impl)
}
}
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, *)
func peripheral(_ peripheral: CBPeripheral,
didOpen channel: CBL2CAPChannel?,
error: Error?) {
impl.delegate?.peripheral(impl, didOpen: channel, error: error)
}
/// Updates the local list of serivces with received ones.
/// - Parameter services: New list of services.
private func smartCopy(_ services: [CBService]?) {
guard let services = services else {
return
}
// So far the "smart" copy isn't that smart and just replaces
// all old references with new ones. The old should still work,
// as they have the correct native references and isEqual also
// compares them. But ideally, the copy should only add new
// attributes, without replacing any existing.
// TODO: Implement smart copy of services.
impl.mockServices = services.map { CBMServiceNative($0, in: impl) }
}
/// Returns the wrapper for the native CBService.
/// - Parameter service: The native service.
private func mock(of service: CBService?) -> CBMServiceNative? {
return impl.mockServices?.first { $0.service == service }
}
/// Returns the wrapper for the native CBCharacteristic.
/// - Parameter characteristic: The native characteristic.
private func mock(of characteristic: CBCharacteristic?) -> CBMCharacteristicNative? {
let service = mock(of: characteristic?.service)
return (service?._characteristics as? [CBMCharacteristicNative])?
.first { $0.characteristic == characteristic }
}
/// Returns the wrapper for the native CBDescriptor.
/// - Parameter descriptor: The native descriptor.
private func mock(of descriptor: CBDescriptor?) -> CBMDescriptorNative? {
let characteristic = mock(of: descriptor?.characteristic)
return (characteristic?._descriptors as? [CBMDescriptorNative])?
.first { $0.descriptor == descriptor }
}
/// Calls the action with mock service.
/// - Parameters:
/// - service: The native service.
/// - action: The action to perform on its mock.
private func usingMock(of service: CBService?,
action: @escaping (CBMPeripheral, CBMPeripheralDelegate, CBMService) -> ()) {
if let delegate = impl.delegate,
let serviceMock = mock(of: service) {
action(impl, delegate, serviceMock)
}
}
/// Calls the action with mock characteristic.
/// - Parameters:
/// - service: The native characteristic.
/// - action: The action to perform on its mock.
private func usingMock(of characteristic: CBCharacteristic?,
action: @escaping (CBMPeripheral, CBMPeripheralDelegate, CBMCharacteristic) -> ()) {
usingMock(of: characteristic?.service) { p, d, s in
if let characteristicMock = self.mock(of: characteristic) {
action(p, d, characteristicMock)
}
}
}
/// Calls the action with mock descriptor.
/// - Parameters:
/// - service: The native descriptor.
/// - action: The action to perform on its mock.
private func usingMock(of descriptor: CBDescriptor?,
action: @escaping (CBMPeripheral, CBMPeripheralDelegate, CBMDescriptor) -> ()) {
usingMock(of: descriptor?.characteristic) { p, d, c in
if let descriptorMock = self.mock(of: descriptor) {
action(p, d, descriptorMock)
}
}
}
}
private var wrapper: CBPeripheralDelegate?
public weak var delegate: CBMPeripheralDelegate? {
didSet {
if let _ = delegate {
// We need to hold a strong reference to the wrapper, otherwise
// it would be immediately deallocated.
wrapper = CBPeripheralDelegateWrapper(self)
peripheral.delegate = wrapper
} else {
wrapper = nil
peripheral.delegate = nil
}
}
}
/// The unique, persistent identifier associated with the peer.
public override var identifier: UUID {
return peripheral.identifier
}
public var name: String? {
return peripheral.name
}
public var state: CBMPeripheralState {
return peripheral.state
}
private var mockServices: [CBMServiceNative]?
public var services: [CBMService]? {
return mockServices
}
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, *)
public var canSendWriteWithoutResponse: Bool {
return peripheral.canSendWriteWithoutResponse
}
#if !os(macOS)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public var ancsAuthorized: Bool {
return peripheral.ancsAuthorized
}
#endif
fileprivate let peripheral: CBPeripheral
fileprivate init(_ peripheral: CBPeripheral) {
self.peripheral = peripheral
}
public func readRSSI() {
peripheral.readRSSI()
}
public func discoverServices(_ serviceUUIDs: [CBMUUID]?) {
peripheral.discoverServices(serviceUUIDs)
}
public func discoverIncludedServices(_ includedServiceUUIDs: [CBMUUID]?,
for service: CBMService) {
if let n = service as? CBMServiceNative {
peripheral.discoverIncludedServices(includedServiceUUIDs, for: n.service)
}
}
public func discoverCharacteristics(_ characteristicUUIDs: [CBMUUID]?,
for service: CBMService) {
if let n = service as? CBMServiceNative {
peripheral.discoverCharacteristics(characteristicUUIDs, for: n.service)
}
}
public func discoverDescriptors(for characteristic: CBMCharacteristic) {
if let n = characteristic as? CBMCharacteristicNative {
peripheral.discoverDescriptors(for: n.characteristic)
}
}
public func readValue(for characteristic: CBMCharacteristic) {
if let n = characteristic as? CBMCharacteristicNative {
peripheral.readValue(for: n.characteristic)
}
}
public func readValue(for descriptor: CBMDescriptor) {
if let n = descriptor as? CBMDescriptorNative {
peripheral.readValue(for: n.descriptor)
}
}
@available(iOS 9.0, *)
public func maximumWriteValueLength(for type: CBMCharacteristicWriteType) -> Int {
return peripheral.maximumWriteValueLength(for: type)
}
public func writeValue(_ data: Data, for characteristic: CBMCharacteristic,
type: CBMCharacteristicWriteType) {
if let n = characteristic as? CBMCharacteristicNative {
peripheral.writeValue(data, for: n.characteristic, type: type)
}
}
public func writeValue(_ data: Data, for descriptor: CBMDescriptor) {
if let n = descriptor as? CBMDescriptorNative {
peripheral.writeValue(data, for: n.descriptor)
}
}
public func setNotifyValue(_ enabled: Bool,
for characteristic: CBMCharacteristic) {
if let n = characteristic as? CBMCharacteristicNative {
peripheral.setNotifyValue(enabled, for: n.characteristic)
}
}
#if !os(macOS)
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, *)
public func openL2CAPChannel(_ PSM: CBML2CAPPSM) {
peripheral.openL2CAPChannel(PSM)
}
#endif
public override var hash: Int {
return identifier.hashValue
}
}