diff options
Diffstat (limited to 'chromium/third_party/nearby/src/internal/platform/implementation/ios')
52 files changed, 9407 insertions, 127 deletions
diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/BUILD b/chromium/third_party/nearby/src/internal/platform/implementation/ios/BUILD index 8d2f4a5bcbe..910ddc319c5 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/BUILD +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/BUILD @@ -15,6 +15,7 @@ licenses(["notice"]) package(default_visibility = [ "//connections/clients/ios:__subpackages__", + "//connections/clients/swift:__subpackages__", "//internal/platform/implementation/ios:__subpackages__", ]) @@ -42,13 +43,12 @@ objc_library( "wifi_lan.h", ], features = ["-layering_check"], - sdk_frameworks = [ - "CoreBluetooth", - "CoreFoundation", - ], deps = [ ":Platform_cc", ":Shared", + "//third_party/apple_frameworks:CoreBluetooth", + "//third_party/apple_frameworks:CoreFoundation", + "//third_party/apple_frameworks:Foundation", "//internal/platform/implementation:platform", "//internal/platform/implementation:types", "//internal/platform/implementation/ios/Mediums", @@ -66,7 +66,7 @@ objc_library( "GNCUtils.h", ], deps = [ - "//third_party/objective_c/google_toolbox_for_mac:GTM_StringEncoding", + "//third_party/apple_frameworks:Foundation", "@aappleby_smhasher//:libmurmur3", ], ) diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/BUILD b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/BUILD index 435773a4bef..1fb35173f46 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/BUILD +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/BUILD @@ -1,5 +1,3 @@ -load("//tools/build_defs/apple:objc.bzl", "objc_proto_library") - # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,7 +19,9 @@ objc_library( name = "Mediums", srcs = [ "Ble/GNCMBleCentral.m", + "Ble/GNCMBleConnection.m", "Ble/GNCMBlePeripheral.m", + "Ble/GNCMBleUtils.mm", "GNCLeaks.m", "GNCMConnection.m", "WifiLan/GNCMBonjourBrowser.m", @@ -31,7 +31,9 @@ objc_library( ], hdrs = [ "Ble/GNCMBleCentral.h", + "Ble/GNCMBleConnection.h", "Ble/GNCMBlePeripheral.h", + "Ble/GNCMBleUtils.h", "GNCLeaks.h", "GNCMConnection.h", "WifiLan/GNCMBonjourBrowser.h", @@ -40,21 +42,14 @@ objc_library( "WifiLan/GNCMBonjourUtils.h", ], deps = [ - ":ObjCProtos", + "//third_party/apple_frameworks:CoreBluetooth", + "//third_party/apple_frameworks:Foundation", "//internal/platform/implementation/ios:Shared", + "//internal/platform/implementation/ios/Mediums/Ble/Sockets:Central", + "//internal/platform/implementation/ios/Mediums/Ble/Sockets:Peripheral", + "//internal/platform/implementation/ios/Mediums/Ble/Sockets:Shared", + "//proto/mediums:ble_frames_cc_proto", "//third_party/objective_c/google_toolbox_for_mac:GTM_Logger", "@com_google_absl//absl/numeric:int128", ], ) - -objc_proto_library( - name = "ObjCProtos", - deps = [":Protos"], -) - -proto_library( - name = "Protos", - deps = [ - "//connections/implementation/proto:offline_wire_formats_proto", - ], -) diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.h index 8d87ec92fd6..e40b783f84a 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.h +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.h @@ -12,16 +12,43 @@ // See the License for the specific language governing permissions and // limitations under the License. +#import <CoreBluetooth/CoreBluetooth.h> + #import "internal/platform/implementation/ios/Mediums/GNCMConnection.h" -#import <Foundation/Foundation.h> +@class CBUUID; NS_ASSUME_NONNULL_BEGIN /** * This handler is called on a discover when a nearby advertising endpoint is discovered. */ -typedef void (^GNCMScanResultHandler)(NSString *serviceUUID, NSData *serviceData); +typedef void (^GNCMScanResultHandler)(NSString *peripheralID, NSData *serviceData); + +/** + * This handler is called on a discovery when a nearby advertising endpoint is connected. + */ +typedef void (^GNCMGATTConnectionResultHandler)(NSError *_Nullable error); + +/** + * This handler is called on a discovery when a nearby advertising endpoint is connected. The input + * is a discovered set of characteristics. + */ +typedef void (^GNCMGATTDiscoverResultHandler)( + NSOrderedSet<CBCharacteristic *> *_Nullable characteristicValues); + +/** + * This handler is called by a discovering endpoint to request a connection with an an advertising + * endpoint. + */ +typedef void (^GNCMBleConnectionRequester)(NSString *serviceID, + GNCMConnectionHandler connectionHandler); + +/** + * This handler is called on a discoverer when a nearby advertising endpoint is discovered. + * Call |requestConnection| to request a connection with the advertiser. + */ +typedef void (^GNCMBleRequestConnectionHandler)(GNCMBleConnectionRequester requestConnection); /** * GNCMBleCentral discovers devices advertising the specified service UUID via BLE (using the @@ -33,18 +60,54 @@ typedef void (^GNCMScanResultHandler)(NSString *serviceUUID, NSData *serviceData */ @interface GNCMBleCentral : NSObject -- (instancetype)init NS_UNAVAILABLE; +- (instancetype)init NS_DESIGNATED_INITIALIZER; /** - * Initializes an `GNCMBleCentral` object. + * Starts scanning with service UUID. * * @param serviceUUID A string that uniquely identifies the scanning services to search for. * @param scanResultHandler The handler that is called when an endpoint advertising the service * UUID is discovered. + * @param requestConnectionHandler The handler that is called when an endpoint is discovered. + * @param callbackQueue The queue on which all callbacks are made. + */ +- (BOOL)startScanningWithServiceUUID:(NSString *)serviceUUID + scanResultHandler:(GNCMScanResultHandler)scanResultHandler + requestConnectionHandler:(GNCMBleRequestConnectionHandler)requestConnectionHandler + callbackQueue:(nullable dispatch_queue_t)callbackQueue; + +/** + * Sets up a GATT connection. + * + * @param peripheralID A string that uniquely identifies the peripheral. + * @param gattConnectionResultHandler The handler that is called when an endpoint is + * connected. + */ +- (void)connectGattServerWithPeripheralID:(NSString *)peripheralID + gattConnectionResultHandler: + (GNCMGATTConnectionResultHandler)gattConnectionResultHandler; + +/** + * Discovers GATT service and its associated characteristics with values. + * + * @param serviceUUID A CBUUID for service to discover. + * @param gattCharacteristics Array of CBUUID for characteristic to discover. + * @param peripheralID A string that uniquely identifies the peripheral. + * @param gattDiscoverResultHandler This handler is called on a discovery for a discovered map of + * characteristic values when a nearby advertising endpoint is + * connected. + */ +- (void)discoverGattService:(CBUUID *)serviceUUID + gattCharacteristics:(NSArray<CBUUID *> *)characteristicUUIDs + peripheralID:(NSString *)peripheralID + gattDiscoverResultHandler:(GNCMGATTDiscoverResultHandler)gattDiscoverResultHandler; + +/** + * Disconnects GATT connection. + * + * @param peripheralID A string that uniquely identifies the peripheral. */ -- (instancetype)initWithServiceUUID:(NSString *)serviceUUID - scanResultHandler:(GNCMScanResultHandler)scanResultHandler - NS_DESIGNATED_INITIALIZER; +- (void)disconnectGattServiceWithPeripheralID:(NSString *)peripheralID; @end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.m b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.m index 5feb359806f..2148a45fddf 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.m +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.m @@ -14,13 +14,71 @@ #import "internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.h" -#include <CoreBluetooth/CoreBluetooth.h> - +#import "internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.h" +#import "internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.h" #import "internal/platform/implementation/ios/Mediums/GNCMConnection.h" NS_ASSUME_NONNULL_BEGIN -@interface GNCMBleCentral () <CBCentralManagerDelegate, CBPeripheralDelegate> +typedef NS_ENUM(NSUInteger, GNCMCentralState) { + GNCMCentralStateStopped, + GNCMCentralStateScanning, +}; + +typedef void (^GNCMBleCharacteristicsHandler)(NSArray<CBCharacteristic *> *characteristics, + NSError *error); +typedef void (^GNCMBleCharacteristicValueHandler)(CBCharacteristic *characteristic, NSError *error); +typedef void (^GNCIntHandler)(int); + +/** This lets a GNCIntHandler call itself. */ +GNCIntHandler GNCRecursiveIntHandler(void (^block)(GNCIntHandler blockSelf, int i)) { + return ^(int i) { + return block(GNCRecursiveIntHandler(block), i); + }; +} + +/** This represents a discovered peripheral. */ +@interface GNCMPeripheralInfo : NSObject +@property(nonatomic) CBPeripheral *peripheral; +@property(nonatomic, copy) NSDictionary<NSString *, id> *advertisementData; + +/** Called when characteristics are discovered. */ +@property(nonatomic, nullable) GNCMBleCharacteristicsHandler charsHandler; + +/** Called when a characteristic value is read. */ +@property(nonatomic, nullable) GNCMBleCharacteristicValueHandler charValueHandler; + +@end + +@implementation GNCMPeripheralInfo + +- (instancetype)initWithPeripheral:(CBPeripheral *)peripheral + advertisementData:(NSDictionary<NSString *, id> *)advertisementData { + self = [super init]; + if (self) { + _peripheral = peripheral; + _advertisementData = [advertisementData copy]; + } + return self; +} + +- (BOOL)isEqual:(id)object { + // There is always exactly one info object per peripheral, so compare by identity. This is needed + // for maintenance of the peripherals stored in multiple maps. + return self == object; +} + +- (NSUInteger)hash { + return (NSUInteger)self; +} + +@end + +@interface GNCMBleCentral () <CBCentralManagerDelegate, + CBPeripheralDelegate, + GNSCentralManagerDelegate> @end @implementation GNCMBleCentral { @@ -32,22 +90,40 @@ NS_ASSUME_NONNULL_BEGIN CBCentralManager *_centralManager; /** Serial background queue for |centralManager|. */ dispatch_queue_t _selfQueue; + /** Central state for stop or scanning. */ + GNCMCentralState _centralState; + /** The dictionary keyed by CBPeripheral identifier to value GNCMPeripheralInfo. */ + NSMutableDictionary<NSUUID *, GNCMPeripheralInfo *> *_nearbyPeripheralsByID; + /** Array of characteristic UUID used for discovering. */ + NSArray<CBUUID *> *_characteristicUUIDs; + /** GATT connection result handler. */ + GNCMGATTConnectionResultHandler _gattConnectionResultHandler; + /** GATT service and characteristic discovery result hanadler. */ + GNCMGATTDiscoverResultHandler _gattDiscoverResultHandler; + /** The discovered characteristic values map used to callback for `_gattDiscoverResultHandler`. */ + NSMutableOrderedSet<CBCharacteristic *> *_gattCharacteristicValues; + /** Central manager used for socket connection based on weave protocol. */ + GNSCentralManager *_socketCentralManager; + /** A callback handler to reuqest connection on the discovered advertiser. */ + GNCMBleRequestConnectionHandler _requestConnectionHandler; + /** Client callback queue. If client doesn't assign it, then use main queue. */ + dispatch_queue_t _clientCallbackQueue; + /** Internal async priority queue. */ + dispatch_queue_t _internalCallbackQueue; + /** Flag to disable callback for dealloc. */ + BOOL _callbacksEnabled; } -- (instancetype)initWithServiceUUID:(NSString *)serviceUUID - scanResultHandler:(GNCMScanResultHandler)scanResultHandler { - self = [super init]; - if (self) { - _serviceUUID = [CBUUID UUIDWithString:serviceUUID]; - _scanResultHandler = scanResultHandler; - +- (instancetype)init { + if (self = [super init]) { // To make this class thread-safe, use a serial queue for all state changes, and have Core // Bluetooth also use this queue. _selfQueue = dispatch_queue_create("GNCCentralManagerQueue", DISPATCH_QUEUE_SERIAL); - _centralManager = [[CBCentralManager alloc] - initWithDelegate:self - queue:_selfQueue - options:@{CBCentralManagerOptionShowPowerAlertKey : @NO}]; + + _nearbyPeripheralsByID = [NSMutableDictionary dictionary]; + _gattCharacteristicValues = [[NSMutableOrderedSet alloc] init]; + + _centralState = GNCMCentralStateStopped; } return self; } @@ -57,24 +133,378 @@ NS_ASSUME_NONNULL_BEGIN // dispatch_sync. This means dealloc must be called from an external queue, which means |self| // must never be captured by any escaping block used in this class. dispatch_sync(_selfQueue, ^{ - [_centralManager stopScan]; + [self stopScanningInternal]; + [_socketCentralManager stopNoScanMode]; + + _callbacksEnabled = NO; + }); +} + +- (BOOL)startScanningWithServiceUUID:(NSString *)serviceUUID + scanResultHandler:(GNCMScanResultHandler)scanResultHandler + requestConnectionHandler:(GNCMBleRequestConnectionHandler)requestConnectionHandler + callbackQueue:(nullable dispatch_queue_t)callbackQueue { + NSLog(@"[NEARBY] Client rquests startScanning"); + _serviceUUID = [CBUUID UUIDWithString:serviceUUID]; + _scanResultHandler = scanResultHandler; + _requestConnectionHandler = requestConnectionHandler; + + // The client may be using the callback queue for other purposes, so wrap it with a private + // queue to know with certainty when all callbacks are done. + _clientCallbackQueue = callbackQueue ?: dispatch_get_main_queue(); + _internalCallbackQueue = + dispatch_queue_create("GNCMBleCentralCallbackQueue", DISPATCH_QUEUE_PRIORITY_DEFAULT); + _callbacksEnabled = YES; + + // Set up the central manager for scanning. + _centralManager = + [[CBCentralManager alloc] initWithDelegate:self + queue:_selfQueue + options:@{CBCentralManagerOptionShowPowerAlertKey : @NO}]; + + // Set up the central manager for the socket. + _socketCentralManager = [[GNSCentralManager alloc] initWithSocketServiceUUID:_serviceUUID + queue:_selfQueue]; + _socketCentralManager.delegate = self; + [_socketCentralManager startNoScanModeWithAdvertisedServiceUUIDs:@[ _serviceUUID ]]; + + _centralState = GNCMCentralStateScanning; + return YES; +} + +- (void)connectGattServerWithPeripheralID:(NSString *)peripheralID + gattConnectionResultHandler: + (GNCMGATTConnectionResultHandler)gattConnectionResultHandler { + _gattConnectionResultHandler = gattConnectionResultHandler; + dispatch_sync(_selfQueue, ^{ + GNCMPeripheralInfo *peripheralInfo = + _nearbyPeripheralsByID[[[NSUUID alloc] initWithUUIDString:peripheralID]]; + if (!peripheralInfo) return; + [_centralManager connectPeripheral:peripheralInfo.peripheral options:nil]; + }); +} + +- (void)discoverGattService:(CBUUID *)serviceUUID + gattCharacteristics:(NSArray<CBUUID *> *)characteristicUUIDs + peripheralID:(NSString *)peripheralID + gattDiscoverResultHandler:(GNCMGATTDiscoverResultHandler)gattDiscoverResultHandler { + _gattDiscoverResultHandler = gattDiscoverResultHandler; + [_gattCharacteristicValues removeAllObjects]; + dispatch_sync(_selfQueue, ^{ + GNCMPeripheralInfo *peripheralInfo = + _nearbyPeripheralsByID[[[NSUUID alloc] initWithUUIDString:peripheralID]]; + if (!peripheralInfo) return; + _characteristicUUIDs = [characteristicUUIDs copy]; + + // Start to discover service and the delegate will get its characteristics and read their values + // recursively + [peripheralInfo.peripheral discoverServices:@[ serviceUUID ]]; + }); +} + +- (void)disconnectGattServiceWithPeripheralID:(NSString *)peripheralID { + dispatch_sync(_selfQueue, ^{ + GNCMPeripheralInfo *peripheralInfo = + _nearbyPeripheralsByID[[[NSUUID alloc] initWithUUIDString:peripheralID]]; + if (!peripheralInfo) return; + _gattConnectionResultHandler = nil; + _gattDiscoverResultHandler = nil; + [_centralManager cancelPeripheralConnection:peripheralInfo.peripheral]; }); } #pragma mark CBCentralManagerDelegate - (void)centralManagerDidUpdateState:(CBCentralManager *)central { - if (central.state == CBManagerStatePoweredOn) { + if (central.state == CBManagerStatePoweredOn && !central.isScanning && + _centralState == GNCMCentralStateScanning) { NSLog(@"[NEARBY] CBCentralManager powered on; starting scan"); + [self startScanningInternal]; + } else { + NSLog(@"[NEARBY] CBCentralManager not powered on; stopping scan"); + [self stopScanningInternal]; + } +} + +- (void)centralManager:(CBCentralManager *)central + didDiscoverPeripheral:(CBPeripheral *)peripheral + advertisementData:(NSDictionary<NSString *, id> *)advertisementData + RSSI:(NSNumber *)RSSI { + NSNumber *connectable = advertisementData[CBAdvertisementDataIsConnectable]; + if (![connectable boolValue]) return; + + // Look for the NC advertisement header in either the service data (from non-iOS) or the + // advertised name (from iOS). + NSData *serviceData = advertisementData[CBAdvertisementDataServiceDataKey][_serviceUUID] + ?: advertisementData[CBAdvertisementDataLocalNameKey]; + + // Try to look up the peripheral by ID. + GNCMPeripheralInfo *info = _nearbyPeripheralsByID[peripheral.identifier]; + if (!info) { + NSLog(@"[NEARBY] New peripheral: %@", peripheral); + // This is a new peripheral, so create a new peripheral info object. + info = [[GNCMPeripheralInfo alloc] initWithPeripheral:peripheral + advertisementData:advertisementData]; + } else { + info.peripheral = peripheral; + } + + _nearbyPeripheralsByID[peripheral.identifier] = info; + _scanResultHandler(peripheral.identifier.UUIDString, serviceData); +} + +- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral { + NSLog(@"[NEARBY] Connected to peripheral: %@", peripheral); + peripheral.delegate = self; + // Tell the caller the connection is done. + _gattConnectionResultHandler(nil); +} + +- (void)centralManager:(CBCentralManager *)central + didFailToConnectPeripheral:(CBPeripheral *)peripheral + error:(nullable NSError *)error { + NSLog(@"[NEARBY] Failed to connect to peripheral: %@, error: %@", peripheral, error); + // Tell the caller the connection failed. + _gattConnectionResultHandler(error); +} + +- (void)centralManager:(CBCentralManager *)central + didDisconnectPeripheral:(CBPeripheral *)peripheral + error:(nullable NSError *)error { + NSLog(@"[NEARBY] Disconnected to peripheral: %@, error: %@", peripheral, error); +} + +#pragma mark CBPeripheralDelegate + +- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error { + GNCMPeripheralInfo *peripheralInfo = _nearbyPeripheralsByID[peripheral.identifier]; + if (!peripheralInfo) return; + if (error || (peripheral.services.count == 0)) { + NSLog(@"[NEARBY] Error reading advertisement: unable to discover services."); + _gattDiscoverResultHandler(nil); + } else { + NSLog(@"[NEARBY] Discovered services for %@: %@", peripheral.name, peripheral.services); + + // Helper functions for discovering characteristics and reading their values. + void (^discoverChars)(CBService *, GNCMBleCharacteristicsHandler) = + ^(CBService *service, GNCMBleCharacteristicsHandler handler) { + NSAssert(!peripheralInfo.charsHandler, @"Unexpected characteristic handler"); + peripheralInfo.charsHandler = handler; + + // Discover all characteristics that may contain the advertisement. + [peripheral discoverCharacteristics:_characteristicUUIDs forService:service]; + }; + void (^readCharValue)(CBCharacteristic *, GNCMBleCharacteristicValueHandler) = + ^(CBCharacteristic *characteristic, GNCMBleCharacteristicValueHandler handler) { + NSAssert(!peripheralInfo.charValueHandler, @"Unexpected characteristic value handler"); + peripheralInfo.charValueHandler = handler; + [peripheral readValueForCharacteristic:characteristic]; + }; + + // Multiple services may have the same UUID, so find the right service by searching for the + // characteristic containing the advertisement with a matching service ID hash. + __weak __typeof__(self) weakSelf = self; + void (^tryService)(int) = GNCRecursiveIntHandler(^(GNCIntHandler tryService, int serviceIndex) { + __strong __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) return; + + // We've tried all the services, report the discovered characteristics and their values. + if (serviceIndex == peripheral.services.count) { + NSLog(@"[NEARBY] Done traversing all services or no services to traverse."); + _gattDiscoverResultHandler(_gattCharacteristicValues); + if (_gattCharacteristicValues.count > 0) { + CBCharacteristic *characteristic = [_gattCharacteristicValues objectAtIndex:0]; + [strongSelf completeGATTReadWithData:characteristic.value + forPeripheralInfo:peripheralInfo]; + } + return; + } + + NSLog(@"[NEARBY] Trying service: %@", peripheral.services[serviceIndex]); + discoverChars( + peripheral.services[serviceIndex], ^(NSArray<CBCharacteristic *> *chars, NSError *error) { + void (^tryNextService)() = ^{ + tryService(serviceIndex + 1); + }; + + // If there was an error or there are no characteristics on this service, try next one. + if (error || (chars.count == 0)) { + tryNextService(); + return; + } + + // Read each characteristic. + void (^tryChar)(int) = GNCRecursiveIntHandler(^(GNCIntHandler tryChar, int charIndex) { + // We've tried all characteristics on this service without error, try next service. + if (charIndex == chars.count) { + NSLog(@"[NEARBY] No matching advertisement found"); + tryNextService(); + return; + } + + NSLog(@"[NEARBY] Trying characteristic: %@", chars[charIndex]); + readCharValue(chars[charIndex], ^(CBCharacteristic *characteristic, NSError *error) { + if (error) { + tryNextService(); + } else { + // We've found the characteristic and its non-nil value. Store it. + if (characteristic.value.length != 0) { + [_gattCharacteristicValues addObject:characteristic]; + } + tryChar(charIndex + 1); + } + }); + }); + + // Start searching the characteristics for the current service. + tryChar(0); + }); + }); + + // Start searching the services. + tryService(0); + } +} + +- (void)peripheral:(CBPeripheral *)peripheral + didDiscoverCharacteristicsForService:(CBService *)service + error:(nullable NSError *)error { + NSLog(@"[NEARBY] Discovered characteristics: %@ error: %@", service.characteristics, error); + GNCMPeripheralInfo *peripheralInfo = _nearbyPeripheralsByID[peripheral.identifier]; + if (!peripheralInfo) return; + GNCMBleCharacteristicsHandler charsHandler = peripheralInfo.charsHandler; + peripheralInfo.charsHandler = nil; + charsHandler(service.characteristics, error); +} + +- (void)peripheral:(CBPeripheral *)peripheral + didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic + error:(nullable NSError *)error { + NSLog(@"[NEARBY] Read characteristic value: %@ error: %@", characteristic, error); + GNCMPeripheralInfo *peripheralInfo = _nearbyPeripheralsByID[peripheral.identifier]; + if (!peripheralInfo) return; + GNCMBleCharacteristicValueHandler valueHandler = peripheralInfo.charValueHandler; + peripheralInfo.charValueHandler = nil; + valueHandler(characteristic, error); +} + +#pragma mark GNSCentralManagerDelegate + +- (void)centralManager:(GNSCentralManager *)centralManager + didDiscoverPeer:(GNSCentralPeerManager *)centralPeerManager + advertisementData:(nullable NSDictionary<NSString *, id> *)advertisementData { + if (!_callbacksEnabled) return; + + // Retrieve the cached peripheral info. + NSUUID *peerId = centralPeerManager.identifier; + GNCMPeripheralInfo *peripheralInfo = _nearbyPeripheralsByID[peerId]; + if (!peripheralInfo) return; + + __weak __typeof__(self) weakSelf = self; + [self callbackAsync:^{ + _requestConnectionHandler(^(NSString *serviceID, GNCMConnectionHandler connectionHandler) { + __strong __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) return; + + void (^callConnectionHandler)(GNSSocket *__nullable) = ^(GNSSocket *__nullable socket) { + __strong __typeof__(self) strongSelf = weakSelf; + if (!strongSelf->_callbacksEnabled) return; + [strongSelf callbackAsync:^{ + if (!socket) { + NSLog(@"[NEARBY] Central failed to create BLE socket"); + connectionHandler(nil); + } else { + GNCMBleConnection *connection = + [GNCMBleConnection connectionWithSocket:socket + serviceID:serviceID + expectedIntroPacket:NO + callbackQueue:strongSelf->_clientCallbackQueue]; + connection.connectionHandlers = connectionHandler(connection); + } + }]; + }; + + dispatch_async(strongSelf->_selfQueue, ^{ + // A connection is being requested, so establish a BLE socket. Make sure to use the most + // up-to-date GNSCentralPeerManager in case the MAC address rotated. The cached + // peripheral info should be the single source of truth for which MAC address to use. + GNSCentralPeerManager *updatedCentralPeerManager = + [centralManager retrieveCentralPeerWithIdentifier:peripheralInfo.peripheral.identifier]; + if (!updatedCentralPeerManager) { + callConnectionHandler(nil); + } + + // Make a socket connection. + [updatedCentralPeerManager + socketWithPairingCharacteristic:NO + completion:^(GNSSocket *socket, NSError *error) { + __strong __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) return; + dispatch_async(strongSelf->_selfQueue, ^{ + if (!error) { + // Call the connection handler when the socket has + // connected or fails to connect. + GNCMWaitForConnection(socket, ^(BOOL didConnect) { + callConnectionHandler(didConnect ? socket : nil); + }); + } else { + callConnectionHandler(nil); + } + }); + }]; + }); + }); + }]; +} + +- (void)centralManagerDidUpdateBleState:(GNSCentralManager *)centralManager { + // No op. +} + +#pragma mark Private + +/** This method assumes it's being called on selfQueue. */ +- (void)completeGATTReadWithData:(NSData *)advertisement + forPeripheralInfo:(GNCMPeripheralInfo *)peripheralInfo { + NSAssert(peripheralInfo, @"Nil peripheralInfo"); + if (peripheralInfo) { + CBPeripheral *peripheral = peripheralInfo.peripheral; + NSUUID *peripheralID = peripheral.identifier; + // Store the data for the socket callback, and report the peripheral to the socket library. + [_socketCentralManager retrievePeripheralWithIdentifier:peripheralID + advertisementData:peripheralInfo.advertisementData]; + } +} + +/** Signals the central manager to start scanning. Must be called on _selfQueue. */ +- (void)startScanningInternal { + if (![_centralManager isScanning]) { + NSLog(@"[NEARBY] startScanningInternal"); + [_centralManager scanForPeripheralsWithServices:@[ _serviceUUID ] options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES}]; - } else { - NSLog(@"[NEARBY] CBCentralManager not powered on; stopping scan"); + } +} + +/** Signals the central manager to stop scanning. Must be called on _selfQueue */ +- (void)stopScanningInternal { + if ([_centralManager isScanning]) { + NSLog(@"[NEARBY] stopScanningInternal"); + _centralState = GNCMCentralStateStopped; + [_centralManager stopScan]; } } +/** Calls the specified block on the callback queue. */ +- (void)callbackAsync:(dispatch_block_t)block { + dispatch_queue_t clientCallbackQueue = _clientCallbackQueue; // don't capture |self| + dispatch_async(_internalCallbackQueue, ^{ + dispatch_sync(clientCallbackQueue, block); + }); +} + @end NS_ASSUME_NONNULL_END diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.h new file mode 100644 index 00000000000..8897a6d7d81 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.h @@ -0,0 +1,43 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/GNCMConnection.h" + +NS_ASSUME_NONNULL_BEGIN + +@class GNSSocket; + +/** + * A GNCMConnection implementation for BLE. + * This class is thread-safe. + */ +@interface GNCMBleConnection : NSObject <GNCMConnection> +@property(nonatomic) GNCMConnectionHandlers *connectionHandlers; + +/** + * Creates a |GNCMBleConnectiom|. + * + * @param socket A |GNSSocket| instance. + * @param serviceID A string that uniquely identifies the service. + * @param expectedIntroPacket A flag to indicate the connection is expecting the + * introduction packet. + * @param callbackQueue The queue on which all callbacks are made. + */ ++ (instancetype)connectionWithSocket:(GNSSocket *)socket + serviceID:(nullable NSString *)serviceID + expectedIntroPacket:(BOOL)expectedIntroPacket + callbackQueue:(dispatch_queue_t)callbackQueue; +@end + +NS_ASSUME_NONNULL_END diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.m b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.m new file mode 100644 index 00000000000..acb950ede63 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.m @@ -0,0 +1,147 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.h" + +#import "internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.h" +#import "internal/platform/implementation/ios/Mediums/GNCLeaks.h" +#import "internal/platform/implementation/ios/Mediums/GNCMConnection.h" +#import "GoogleToolboxForMac/GTMLogger.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface GNCMBleConnection () <GNSSocketDelegate> +@property(nonatomic) dispatch_queue_t selfQueue; +@property(nonatomic) GNSSocket *socket; +@property(nonatomic) NSData *serviceIDHash; +@property(nonatomic) dispatch_queue_t callbackQueue; +@property(nonatomic) BOOL expectedIntroPacket; +@property(nonatomic) BOOL receivedIntroPacket; +@end + +@implementation GNCMBleConnection + ++ (instancetype)connectionWithSocket:(GNSSocket *)socket + serviceID:(nullable NSString *)serviceID + expectedIntroPacket:(BOOL)expectedIntroPacket + callbackQueue:(dispatch_queue_t)callbackQueue { + GNCMBleConnection *connection = [[GNCMBleConnection alloc] init]; + connection.socket = socket; + socket.delegate = connection; + connection.serviceIDHash = serviceID ? GNCMServiceIDHash(serviceID) : nil; + connection.callbackQueue = callbackQueue; + connection.selfQueue = dispatch_queue_create("GNCMBleConnectionQueue", DISPATCH_QUEUE_SERIAL); + connection.expectedIntroPacket = expectedIntroPacket; + return connection; +} + +- (void)dealloc { + [_socket disconnect]; + GNCVerifyDealloc(_socket, 2); // assert if not deallocated +} + +#pragma mark GNCMConnection +- (void)sendData:(NSData *)data + progressHandler:(GNCMProgressHandler)progressHandler + completion:(GNCMPayloadResultHandler)completion { + dispatch_async(_selfQueue, ^{ + NSMutableData *packet; + if (data.length == 0) { + // Get the Control introduction packet if data length is 0. + NSData *introData = GNCMGenerateBLEFramesIntroductionPacket(_serviceIDHash); + packet = [NSMutableData dataWithData:introData]; + } else { + // Prefix the service ID hash. + packet = [NSMutableData dataWithData:_serviceIDHash]; + [packet appendData:data]; + } + + [_socket sendData:packet + progressHandler:^(float progress) { + // Convert normalized progress value to number of bytes. + dispatch_async(_callbackQueue, ^{ + progressHandler((size_t)(progress * packet.length)); + }); + } + completion:^(NSError *error) { + dispatch_async(_callbackQueue, ^{ + completion(error ? GNCMPayloadResultFailure : GNCMPayloadResultSuccess); + }); + }]; + }); +} + +#pragma mark GNSSocketDelegate + +- (void)socketDidConnect:(GNSSocket *)socket { + GTMLoggerError(@"Unexpected -socketDidConnect: call; should've already happened"); +} + +- (void)socket:(GNSSocket *)socket didDisconnectWithError:(NSError *)error { + dispatch_async(_selfQueue, ^{ + if (_connectionHandlers.disconnectedHandler) { + dispatch_async(_callbackQueue, ^{ + _connectionHandlers.disconnectedHandler(); + }); + } + }); +} + +- (void)socket:(GNSSocket *)socket didReceiveData:(NSData *)data { + // Extract the service ID prefix from each data packet. + NSMutableData *packet; + NSUInteger prefixLength = _serviceIDHash.length; + if (_expectedIntroPacket && !_receivedIntroPacket) { + // Check if the first packet is intro packet. + if (!_serviceIDHash) { + // If _serviceIdHash is nil, then we need to parse the first incoming packet if it conforms to + // introducion packet and extract the serviceIdHash for coming packets. + NSData *serviceIDHash = GNCMParseBLEFramesIntroductionPacket(data); + if (serviceIDHash) { + _serviceIDHash = serviceIDHash; + _receivedIntroPacket = YES; + } else { + GTMLoggerInfo(@"[NEARBY] Input stream: Received wrong intro packet and discarded"); + } + } else { + NSData *introData = GNCMGenerateBLEFramesIntroductionPacket(_serviceIDHash); + if ([data isEqual:introData]) { + _receivedIntroPacket = YES; + } else { + GTMLoggerInfo(@"[NEARBY] Input stream: Received wrong intro packet and discarded"); + } + } + return; + } + + if (![[data subdataWithRange:NSMakeRange(0, prefixLength)] isEqual:_serviceIDHash]) { + GTMLoggerInfo(@"[NEARBY] Input stream: Received wrong data packet and discarded"); + return; + } + packet = [NSMutableData + dataWithData:[data subdataWithRange:NSMakeRange(prefixLength, data.length - prefixLength)]]; + + dispatch_async(_selfQueue, ^{ + if (_connectionHandlers.payloadHandler) { + dispatch_async(_callbackQueue, ^{ + _connectionHandlers.payloadHandler(packet); + }); + } + }); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h index 1c515441e3d..87361cb5704 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h @@ -12,7 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include <CoreBluetooth/CoreBluetooth.h> +#import <Foundation/Foundation.h> + +#import "internal/platform/implementation/ios/Mediums/GNCMConnection.h" + +@class CBCharacteristic; +@class CBUUID; NS_ASSUME_NONNULL_BEGIN @@ -26,16 +31,48 @@ NS_ASSUME_NONNULL_BEGIN */ @interface GNCMBlePeripheral : NSObject -- (instancetype)init NS_UNAVAILABLE; +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +/** + * Adds GATT CBService. + * + * @param serviceUUID A GATT service ID to advertise for. + */ +- (void)addCBServiceWithUUID:(CBUUID *)serviceUUID; + +/** + * Adds GATT CBCharacteristic. + * + * @param characteristic A characteristic CBUUID. + */ +- (void)addCharacteristic:(CBCharacteristic *)characteristic; + +/** + * Updates GATT CBCharacteristic with value. + * + * @param value The NSData to advertise. + * @param characteristicUUID A characteristic CBUUID. + */ +- (void)updateValue:(NSData *)value forCharacteristic:(CBUUID *)characteristicUUID; + +/** + * Stops GATT server service. + */ +- (void)stopGATTService; /** - * Initializes an `GNCMBlePeripheral` object. + * Starts advertising with service UUID and advertisement data. * * @param serviceUUID A string that uniquely identifies the advertised service to search for. * @param advertisementData The data to advertise. + * @param endpointconnectedHandler The handler that is called when a discoverer connects. + * @param callbackQueue The queue on which all callbacks are made. If |callbackQueue| is not + * provided, then the main queue is used in the function. */ -- (instancetype)initWithServiceUUID:(NSString *)serviceUUID - advertisementData:(NSData *)advertisementData NS_DESIGNATED_INITIALIZER; +- (BOOL)startAdvertisingWithServiceUUID:(NSString *)serviceUUID + advertisementData:(NSData *)advertisementData + endpointConnectedHandler:(GNCMConnectionHandler)endpointConnectedHandler + callbackQueue:(nullable dispatch_queue_t)callbackQueue; @end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.m b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.m index f3fcdc2a9c5..3961f7fe715 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.m +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.m @@ -14,43 +14,61 @@ #import "internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h" -#include <CoreBluetooth/CoreBluetooth.h> +#import <CoreBluetooth/CoreBluetooth.h> #import "internal/platform/implementation/ios/GNCUtils.h" +#import "internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.h" +#import "internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager.h" +#import "internal/platform/implementation/ios/Mediums/GNCMConnection.h" NS_ASSUME_NONNULL_BEGIN +typedef NS_ENUM(NSUInteger, GNCMPeripheralState) { + GNCMPeripheralStateStopped, + GNCMPeripheralStateAdvertising, +}; + @interface GNCMBlePeripheral () <CBPeripheralManagerDelegate> @end @implementation GNCMBlePeripheral { - /** GATT service for advertisement. */ + /** Service UUID for advertisement. */ CBMutableService *_advertisementService; + /** GATT service for GATT connection. */ + CBMutableService *_GATTService; + /** GATT characteristics for GATT connection. */ + NSMutableArray<CBCharacteristic *> *_gattCharacteristics; + /** CBUUID characteristic to NSData value dictionary for GATT connection. */ + NSMutableDictionary<CBUUID *, NSData *> *_gattCharacteristicValues; /** Data to be advertised. */ NSData *_advertisementData; /** Peripheral manager used to advertise or connect to peripherals. */ CBPeripheralManager *_peripheralManager; /** Serial background queue for |peripheralManager|. */ dispatch_queue_t _selfQueue; + /** Peripheral state for stop or advertising. */ + GNCMPeripheralState _state; + /** Peripheral manager used for socket connection based on weave protocol. */ + GNSPeripheralManager *_socketPeripheralManager; + /** Peripheral service manager used to manage one BLE service. */ + GNSPeripheralServiceManager *_socketPeripheralServiceManager; + /** Client callback queue. If client doesn't assign it, then use main queue. */ + dispatch_queue_t _clientCallbackQueue; + /** Internal async priority queue. */ + dispatch_queue_t _internalCallbackQueue; + /** Flag to disable callback for dealloc. */ + BOOL _callbacksEnabled; } -- (instancetype)initWithServiceUUID:(NSString *)serviceUUID - advertisementData:(NSData *)advertisementData { - self = [super init]; - if (self) { - _advertisementService = - [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:serviceUUID] primary:YES]; - _advertisementData = [advertisementData copy]; - +- (instancetype)init { + if (self = [super init]) { // To make this class thread-safe, use a serial queue for all state changes, and have Core // Bluetooth also use this queue. _selfQueue = dispatch_queue_create("GNCPeripheralManagerQueue", DISPATCH_QUEUE_SERIAL); - // Set up the peripheral manager for the advertisement data. - _peripheralManager = [[CBPeripheralManager alloc] - initWithDelegate:self - queue:_selfQueue - options:@{CBPeripheralManagerOptionShowPowerAlertKey : @NO}]; + _state = GNCMPeripheralStateStopped; } return self; } @@ -60,22 +78,114 @@ NS_ASSUME_NONNULL_BEGIN // dispatch_sync. This means delloc must be called from an external queue, which means |self| // must never be captured by any escaping block used in this class. dispatch_sync(_selfQueue, ^{ - [self stopAdvertising]; + [self stopAdvertisingInternal]; + + _callbacksEnabled = NO; }); } +- (void)addCBServiceWithUUID:(CBUUID *)serviceUUID { + if (!_GATTService) { + // If it has been called, then don't do it again. Initialize one time. + _GATTService = [[CBMutableService alloc] initWithType:serviceUUID primary:YES]; + _gattCharacteristics = [[NSMutableArray alloc] init]; + _gattCharacteristicValues = [[NSMutableDictionary alloc] init]; + } +} + +- (void)addCharacteristic:(CBCharacteristic *)characteristic { + if (_gattCharacteristics) { + [_gattCharacteristics addObject:characteristic]; + } + if (_gattCharacteristicValues) { + [_gattCharacteristicValues setObject:[[NSData alloc] init] forKey:characteristic.UUID]; + } +} + +- (void)updateValue:(NSData *)value forCharacteristic:(CBUUID *)characteristicUUID { + if ([_gattCharacteristicValues objectForKey:characteristicUUID]) { + [_gattCharacteristicValues setObject:value forKey:characteristicUUID]; + } +} + +- (void)stopGATTService { + if (!_GATTService) return; + dispatch_sync(_selfQueue, ^{ + [_peripheralManager removeService:_GATTService]; + }); +} + +- (BOOL)startAdvertisingWithServiceUUID:(NSString *)serviceUUID + advertisementData:(NSData *)advertisementData + endpointConnectedHandler:(GNCMConnectionHandler)endpointConnectedHandler + callbackQueue:(nullable dispatch_queue_t)callbackQueue { + NSLog(@"[NEARBY] Client rquests startAdvertising"); + // The client may be using the callback queue for other purposes, so wrap it with a private + // queue to know with certainty when all callbacks are done. + _clientCallbackQueue = callbackQueue ?: dispatch_get_main_queue(); + _internalCallbackQueue = + dispatch_queue_create("GNCMBlePeripheralCallbackQueue", DISPATCH_QUEUE_PRIORITY_DEFAULT); + _callbacksEnabled = YES; + + _advertisementService = [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:serviceUUID] + primary:YES]; + _advertisementData = [advertisementData copy]; + + // To make this class thread-safe, use a serial queue for all state changes, and have Core + // Bluetooth also use this queue. + _selfQueue = dispatch_queue_create("GNCPeripheralManagerQueue", DISPATCH_QUEUE_SERIAL); + __weak __typeof__(self) weakSelf = self; + // Set up the peripheral manager for the socket. This must be done before creating the + // peripheral manager for the advertisement data because it's started/stopped in the + // -peripheralManagerDidUpdateState: callback. + _socketPeripheralServiceManager = [[GNSPeripheralServiceManager alloc] + initWithBleServiceUUID:_advertisementService.UUID + addPairingCharacteristic:NO + shouldAcceptSocketHandler:^BOOL(GNSSocket *socket) { + // Call the connection handler when the socket has connected or fails to connect. + GNCMWaitForConnection(socket, ^(BOOL didConnect) { + [weakSelf establishConnectionWithSocket:socket + didConnect:didConnect + endpointConnectedHandler:endpointConnectedHandler]; + }); + return YES; + } + queue:_selfQueue]; + _socketPeripheralManager = [[GNSPeripheralManager alloc] initWithAdvertisedName:nil + restoreIdentifier:nil + queue:_selfQueue]; + [_socketPeripheralManager addPeripheralServiceManager:_socketPeripheralServiceManager + bleServiceAddedCompletion:^(NSError *error) { + NSLog(@"Failed to add service"); + }]; + + // Set up the peripheral manager for the advertisement data. + _peripheralManager = [[CBPeripheralManager alloc] + initWithDelegate:self + queue:_selfQueue + options:@{CBPeripheralManagerOptionShowPowerAlertKey : @NO}]; + + if (_GATTService) { + if (_gattCharacteristics && _gattCharacteristics.count > 0) { + _GATTService.characteristics = _gattCharacteristics; + } + } + _state = GNCMPeripheralStateAdvertising; + return YES; +} + #pragma mark CBPeripheralManagerDelegate - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral { - if (peripheral.state == CBManagerStatePoweredOn) { + NSLog(@"[NEARBY] peripheralManagerDidUpdateState %li", (long)peripheral.state); + if (peripheral.state == CBManagerStatePoweredOn && !peripheral.isAdvertising && + _state == GNCMPeripheralStateAdvertising) { NSLog(@"[NEARBY] CBPeripheralManager powered on; starting advertising"); - [_peripheralManager startAdvertising:@{ - CBAdvertisementDataServiceUUIDsKey : @[ _advertisementService.UUID ], - CBAdvertisementDataLocalNameKey : _advertisementData - }]; + [_socketPeripheralManager start]; + [self startAdvertisingInternal]; } else { NSLog(@"[NEARBY] CBPeripheralManager not powered on; stopping advertising"); - [self stopAdvertising]; + [self stopAdvertisingInternal]; } } @@ -85,7 +195,7 @@ NS_ASSUME_NONNULL_BEGIN NSLog(@"[NEARBY] Error starting advertising: %@,", [error localizedDescription]); return; } - if (_peripheralManager.state != CBPeripheralManagerStatePoweredOn) { + if (_peripheralManager.state != CBManagerStatePoweredOn) { NSLog(@"[NEARBY] Error starting advertising: peripheral manager not on!"); return; } @@ -93,11 +203,89 @@ NS_ASSUME_NONNULL_BEGIN NSLog(@"[NEARBY] Peripheral manager started advertising"); } +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didReceiveReadRequest:(CBATTRequest *)request { + NSLog(@"[NEARBY] peripheralManager:didReceiveReadRequest"); + // This is called when a central asks to read a characteristic's value. + CBATTError error = CBATTErrorAttributeNotFound; + NSData *value = _gattCharacteristicValues[request.characteristic.UUID]; + if (value != nil && value.length > 0) { + if (request.offset > value.length) { + error = CBATTErrorInvalidOffset; + } else { + // Reply with the advertisement data. + NSRange rangeFromOffset = NSMakeRange(request.offset, value.length - request.offset); + request.value = [value subdataWithRange:rangeFromOffset]; + error = CBATTErrorSuccess; + } + } + [_peripheralManager respondToRequest:request withResult:error]; +} + #pragma mark Private +/** Signals the peripheral manager to start advertising. Must be called on _selfQueue */ +- (void)startAdvertisingInternal { + if (![_peripheralManager isAdvertising]) { + NSLog(@"[NEARBY] startAdvertisingInternal"); + + if (_GATTService) { + [_peripheralManager addService:_GATTService]; + } + [_peripheralManager startAdvertising:@{ + CBAdvertisementDataServiceUUIDsKey : @[ _advertisementService.UUID ], + CBAdvertisementDataLocalNameKey : _advertisementData + }]; + } +} + /** Signals the peripheral manager to stop advertising. Must be called on _selfQueue */ -- (void)stopAdvertising { - [_peripheralManager stopAdvertising]; +- (void)stopAdvertisingInternal { + if ([_peripheralManager isAdvertising]) { + NSLog(@"[NEARBY] stopAdvertisingInternal"); + _state = GNCMPeripheralStateStopped; + + if (_GATTService) { + [_peripheralManager removeService:_GATTService]; + } + [_peripheralManager stopAdvertising]; + } +} + +/** + * Connects with socket and callback the |GNCMBleConnection| is established or nil if it is not. + */ +- (void)establishConnectionWithSocket:(GNSSocket *)socket + didConnect:(BOOL)didConnect + endpointConnectedHandler:(GNCMConnectionHandler)endpointConnectedHandler { + if (!_callbacksEnabled) { + return; + } + + [self callbackAsync:^{ + if (!didConnect) { + NSLog(@"[NEARBY] Peripheral failed to create BLE socket"); + endpointConnectedHandler(nil); + } else { + GNCMBleConnection *connection = [GNCMBleConnection connectionWithSocket:socket + serviceID:nil + expectedIntroPacket:YES + callbackQueue:_clientCallbackQueue]; + connection.connectionHandlers = endpointConnectedHandler(connection); + } + }]; +} + +/** + * Calls the specified block on the callback queue, preventing it from being dispatched to the + * client callback queue when callbacks are disabled. And without capturing |self|, since + * callbacks are disabled in dealloc. + */ +- (void)callbackAsync:(dispatch_block_t)block { + dispatch_queue_t clientCallbackQueue = _clientCallbackQueue; // don't capture |self| + dispatch_async(_internalCallbackQueue, ^{ + dispatch_sync(clientCallbackQueue, block); + }); } @end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.h new file mode 100644 index 00000000000..fd85edba56f --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.h @@ -0,0 +1,58 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@class GNSSocket; + +typedef void (^GNCMBoolHandler)(BOOL flag); + +#ifdef __cplusplus +extern "C" { +#endif + +/** Required lengths of certain BLE advertisement fields. */ +typedef NS_ENUM(NSUInteger, GNCMBleAdvertisementLength) { + /** Length of service ID hash data object, used in the BLE advertisement and the packet prefix. */ + GNCMBleAdvertisementLengthServiceIDHash = 3, +}; + +/** Computes a hash from a service ID string. */ +NSData *GNCMServiceIDHash(NSString *serviceID); + +/** Creates the introduction packet for Ble SocketControlFrame. */ +NSData *GNCMGenerateBLEFramesIntroductionPacket(NSData *serviceIDHash); + +/** + * Parses the packet for Ble SocketControlFrame introduction packet and returns + * serviceIdHash if succeed. + */ +NSData *GNCMParseBLEFramesIntroductionPacket(NSData *data); + +/** Creates the disconnection packet for Ble SocketControlFrame. */ +NSData *GNCMGenerateBLEFramesDisconnectionPacket(NSData *serviceIDHash); + +/** + * Calls the completion handler with (a) YES if the GNSSocket connected, or (b) NO if it failed to + * connect for any reason. The completion handler is called on the main queue. + */ +void GNCMWaitForConnection(GNSSocket *socket, GNCMBoolHandler completion); + +#ifdef __cplusplus +} // extern "C" +#endif + +NS_ASSUME_NONNULL_END diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.mm b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.mm new file mode 100644 index 00000000000..c24fa656d60 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.mm @@ -0,0 +1,143 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.h" + +#include <sstream> +#include <string> + +#import "internal/platform/implementation/ios/GNCUtils.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.h" +#include "proto/mediums/ble_frames.pb.h" +#import "GoogleToolboxForMac/GTMLogger.h" + +NS_ASSUME_NONNULL_BEGIN + +static const uint8_t kGNCMControlPacketServiceIDHash[] = {0x00, 0x00, 0x00}; +static const NSTimeInterval kBleSocketConnectionTimeout = 5.0; + +NSData *GNCMServiceIDHash(NSString *serviceID) { + return [GNCSha256String(serviceID) + subdataWithRange:NSMakeRange(0, GNCMBleAdvertisementLengthServiceIDHash)]; +} + +NSData *GNCMGenerateBLEFramesIntroductionPacket(NSData *serviceIDHash) { + ::location::nearby::mediums::SocketControlFrame socket_control_frame; + + socket_control_frame.set_type(::location::nearby::mediums::SocketControlFrame::INTRODUCTION); + auto *introduction_frame = socket_control_frame.mutable_introduction(); + introduction_frame->set_socket_version(::location::nearby::mediums::SocketVersion::V2); + std::string service_id_hash((char *)serviceIDHash.bytes, (size_t)serviceIDHash.length); + introduction_frame->set_service_id_hash(service_id_hash); + + NSMutableData *packet = [NSMutableData dataWithBytes:kGNCMControlPacketServiceIDHash + length:sizeof(kGNCMControlPacketServiceIDHash)]; + std::ostringstream stream; + socket_control_frame.SerializeToOstream(&stream); + NSData *frameData = [NSData dataWithBytes:stream.str().data() length:stream.str().length()]; + [packet appendData:frameData]; + return packet; +} + +NSData *GNCMParseBLEFramesIntroductionPacket(NSData *data) { + ::location::nearby::mediums::SocketControlFrame socket_control_frame; + NSUInteger prefixLength = sizeof(kGNCMControlPacketServiceIDHash); + NSData *packet = [data subdataWithRange:NSMakeRange(prefixLength, data.length - prefixLength)]; + if (socket_control_frame.ParseFromArray(packet.bytes, (int)packet.length)) { + if (socket_control_frame.type() == + ::location::nearby::mediums::SocketControlFrame::INTRODUCTION && + socket_control_frame.has_introduction() && + socket_control_frame.introduction().has_socket_version() && + socket_control_frame.introduction().socket_version() == + ::location::nearby::mediums::SocketVersion::V2 && + socket_control_frame.introduction().has_service_id_hash()) { + std::string service_id_hash = socket_control_frame.introduction().service_id_hash(); + return [NSData dataWithBytes:service_id_hash.data() length:service_id_hash.length()]; + } + } + + return nil; +} + +NSData *GNCMGenerateBLEFramesDisconnectionPacket(NSData *serviceIDHash) { + ::location::nearby::mediums::SocketControlFrame socket_control_frame; + + socket_control_frame.set_type(::location::nearby::mediums::SocketControlFrame::DISCONNECTION); + auto *disconnection_frame = socket_control_frame.mutable_disconnection(); + std::string service_id_hash((char *)serviceIDHash.bytes, (size_t)serviceIDHash.length); + disconnection_frame->set_service_id_hash(service_id_hash); + + NSMutableData *packet = [NSMutableData dataWithBytes:kGNCMControlPacketServiceIDHash + length:sizeof(kGNCMControlPacketServiceIDHash)]; + std::ostringstream stream; + socket_control_frame.SerializeToOstream(&stream); + NSData *frameData = [NSData dataWithBytes:stream.str().data() length:stream.str().length()]; + [packet appendData:frameData]; + return packet; +} + +@interface GNCMBleSocketDelegate : NSObject <GNSSocketDelegate> +@property(nonatomic) GNCMBoolHandler connectedHandler; +@end + +@implementation GNCMBleSocketDelegate + ++ (instancetype)delegateWithConnectedHandler:(GNCMBoolHandler)connectedHandler { + GNCMBleSocketDelegate *connection = [[GNCMBleSocketDelegate alloc] init]; + connection.connectedHandler = connectedHandler; + return connection; +} + +#pragma mark - GNSSocketDelegate + +- (void)socketDidConnect:(GNSSocket *)socket { + _connectedHandler(YES); +} + +- (void)socket:(GNSSocket *)socket didDisconnectWithError:(NSError *)error { + _connectedHandler(NO); +} + +- (void)socket:(GNSSocket *)socket didReceiveData:(NSData *)data { + GTMLoggerError(@"Unexpected -didReceiveData: call"); +} + +@end + +void GNCMWaitForConnection(GNSSocket *socket, GNCMBoolHandler completion) { + // This function passes YES to the completion when the socket has successfully connected, and + // otherwise passes NO to the completion after a timeout of several seconds. We shouldn't retain + // the completion after it's been called, so store it in a __block variable and nil it out once + // the socket has connected. + __block GNCMBoolHandler completionRef = completion; + + // The delegate listens for the socket connection callbacks. It's retained by the block passed to + // dispatch_after below, so it will live long enough to do its job. + GNCMBleSocketDelegate *delegate = + [GNCMBleSocketDelegate delegateWithConnectedHandler:^(BOOL didConnect) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (completionRef) completionRef(didConnect); + completionRef = nil; + }); + }]; + socket.delegate = delegate; + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kBleSocketConnectionTimeout * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + (void)delegate; // make sure it's retained until the timeout + if (completionRef) completionRef(NO); + }); +} + +NS_ASSUME_NONNULL_END diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/BUILD b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/BUILD new file mode 100644 index 00000000000..397d8952b9c --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/BUILD @@ -0,0 +1,87 @@ +load("//tools/build_defs/swift:swift_explicit_module_build_test.bzl", "swift_explicit_module_build_test") + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +licenses(["notice"]) + +package( + default_visibility = ["//visibility:public"], +) + +objc_library( + name = "Central", + srcs = glob([ + "Source/Central/*.m", + ]), + hdrs = glob([ + "Source/Central/*.h", + ]) + [ + "Source/GNSCentral.h", + ], + deps = [ + ":Shared", + "//third_party/apple_frameworks:CoreBluetooth", + "//third_party/apple_frameworks:CoreFoundation", + "//third_party/apple_frameworks:Foundation", + "//third_party/apple_frameworks:QuartzCore", + "//third_party/objective_c/google_toolbox_for_mac:GTM_Logger", + ], +) + +objc_library( + name = "Peripheral", + srcs = glob([ + "Source/Peripheral/*.m", + ]), + hdrs = glob([ + "Source/Peripheral/*.h", + ]) + [ + "Source/GNSPeripheral.h", + ], + deps = [ + ":Shared", + "//third_party/apple_frameworks:CoreBluetooth", + "//third_party/apple_frameworks:CoreFoundation", + "//third_party/apple_frameworks:Foundation", + "//third_party/apple_frameworks:QuartzCore", + "//third_party/apple_frameworks:UIKit", + "//third_party/objective_c/google_toolbox_for_mac:GTM_Logger", + ], +) + +objc_library( + name = "Shared", + srcs = glob([ + "Source/Shared/*.m", + ]), + hdrs = glob([ + "Source/Shared/*.h", + ]) + [ + "Source/GNSShared.h", + ], + deps = [ + "//third_party/apple_frameworks:CoreBluetooth", + "//third_party/apple_frameworks:CoreFoundation", + "//third_party/apple_frameworks:Foundation", + "//third_party/apple_frameworks:QuartzCore", + "//third_party/objective_c/google_toolbox_for_mac:GTM_Logger", + ], +) + +swift_explicit_module_build_test( + name = "swift_explicit_module_build_test", + ignore_headerless_targets = True, + minimum_os_version = "8.0", + platform_type = "ios", +) diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager+Private.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager+Private.h new file mode 100644 index 00000000000..7735c4b61be --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager+Private.h @@ -0,0 +1,38 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.h" + +#import <CoreBluetooth/CoreBluetooth.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface GNSCentralManager ()<CBCentralManagerDelegate> + +- (void)connectPeripheralForPeer:(GNSCentralPeerManager *)peer + options:(nullable NSDictionary *)options; + +- (void)cancelPeripheralConnectionForPeer:(GNSCentralPeerManager *)peer; + +- (void)centralPeerManagerDidDisconnect:(GNSCentralPeerManager *)peer; + +@end + +@interface GNSCentralManager (VisibleForTesting) + +- (CBCentralManager *)testing_cbCentralManager; + +@end + +NS_ASSUME_NONNULL_END diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.h new file mode 100644 index 00000000000..5ebdbf2c818 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.h @@ -0,0 +1,201 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import <CoreBluetooth/CoreBluetooth.h> +#import <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@class CBUUID; +@class GNSCentralManager; +@class GNSCentralPeerManager; + +@protocol GNSCentralManagerDelegate<NSObject> + +/** + * Called while scanning when a new peer is found. + * + * @param centralManager Central manager + * @param centralPeerManager New central peer. + * @param advertisementData The advertisement data received from the peer. + */ +- (void)centralManager:(GNSCentralManager *)centralManager + didDiscoverPeer:(GNSCentralPeerManager *)centralPeerManager + advertisementData:(nullable NSDictionary *)advertisementData; + +/** + * Called when the Bluetooth Low Energy state is updated. Note that scanning is paused if Bluetooth + * is disabled while scanning is on. Scanning will be resumed automatically as soon as Bluetooth is + * re-enabled. + * + * @param centralManager Central manager + */ +- (void)centralManagerDidUpdateBleState:(GNSCentralManager *)centralManager; + +@end + +/** + * GNSCentralManager can search for new GNSCentralPeerManager or can retrieve known + * GNSCentralPeerManager. Note that GNSCentralPeerManager retains its GNSCentralManager. Therefore + * it is not necessary to keep a strong pointer to GNSCentralManager. + * + * * To create a GNSCentralManager instance: + * GNSCentralManager *centralManager = [[GNSCentralManager alloc] initWithSocketServiceUUID:UUID]; + * centralManager.delegate = myManagerDelegate; + * + * * Get a peer: + * - To retrieve a known peer: + * With a known peer: -[GNSCentralManager retrieveCentralPeerWithIdentifier:] + * To retrieve a previous peer, can be called instead of scanning. + * + * - To scan for peers: + * -[GNSCentralManager startScanWithAdvertisedName:advertisedServiceUUID:] + * When a peer has been discovered, myManagerDelegate is called: + * -[myManagerDelegate centralManager:discoveredPeer:]; + * + * * To create a socket to the peer: + * With the received peer, -[GNSCentralPeerManager socketWithPairingCharacteristic:completion:] + * has to be called: + * [centralPeer socketWithPairingCharacteristic:shouldAddPairingCharacteristics + * completion:^(GNSSocket *mySocket, NSError *error) { + * if (error) { + * NSLog(@"Error to get the socket %@", error); + * return; + * } + * mySocket.delegate = mySocketDelegate; + * }]; + * + * To use the socket, please see GNSSocket. + * + * Once the socket is disconnected, the peer will automatically be disconnected from BLE. Also, if + * GNSCentralPeerManager instance is deallocated, the socket will be automatically closed. + * + * This class is not thread-safe. + */ +@interface GNSCentralManager : NSObject + +/** + * Service used for the socket. + */ +@property(nonatomic, readonly) CBUUID *socketServiceUUID; + +/** + * YES, if scanning is enabled (even when the bluetooth is disabled). + */ +@property(nonatomic, readonly, getter=isScanning) BOOL scanning; +@property(nonatomic, readwrite, weak) id<GNSCentralManagerDelegate> delegate; + +/** + * BLE state based on -[CBCentralManager state]. + */ +@property(nonatomic, readonly) CBCentralManagerState cbCentralManagerState; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Creates an instance of GNSCentralManager. Only one service is supported. This service will be + * passed to the GNSCentralPeerManager instances and GNSSocket. + * + * @param socketServiceUUID UUID used to create peer sockets with all the peers found. + * @param queue The queue this object is called on and callbacks are made on. + * + * @return GNSCentralManager instance + */ +- (nullable instancetype)initWithSocketServiceUUID:(CBUUID *)socketServiceUUID + queue:(dispatch_queue_t)queue + NS_DESIGNATED_INITIALIZER; + +/** + * Creates an instance of GNSCentralManager using the main queue for callbacks. + */ +- (nullable instancetype)initWithSocketServiceUUID:(CBUUID *)socketServiceUUID; + +/** + * TODO(sacomoto): Cleanup and merge this and the method below. + * + * Starts scanning for all Bluetooth Low Energy peers that advertise the service + * |advertisedServiceUUID| or the name |advertisedName|. + * + * If |advertisedName| is non-nil, this central manager will scan for all peripherals and filter + * peripherals that advertise |advertisedServiceUUID| or |advertisedName|. + * + * If |advertisedServiceUUID| is non-nil, the central manager will scan for all peripherals that + * advertise |self.socketServiceUUID|. + * + * If both |advertisedName| and |advertisedServiceUUID| are nil, the central manager will scan for + * all peripherals without any filtering. + * + * Note that |self.socketServiceUUID| and |advertisedServiceUUID| may be different. In this case, + * the manager will scan for devices advertising |advertisedServiceUUID| but will use + * |self.socketServiceUUID| to transfer data. + * + * The delegate will be notified when each peer is found. + * + * @param advertisedName The name advertised by peripheral. + * @param advertisedServiceUUID The service advertised by the peripheral. + */ +- (void)startScanWithAdvertisedName:(nullable NSString *)advertisedName + advertisedServiceUUID:(nullable CBUUID *)advertisedServiceUUID; + +/** + * Starts scanning for all Bluetooth Low Energy peers that advertise some service in + * |advertisedServiceUUIDs|. The delegate will be notified when each peer is found. + * + * @param advertisedServiceUUID The service advertised by the peripheral. + */ +- (void)startScanWithAdvertisedServiceUUIDs:(nullable NSArray<CBUUID *> *)advertisedServiceUUIDs; + +/** + * Stops scanning. + */ +- (void)stopScan; + +/** + * Starts "no-scan" mode, where identifiers of peripherals already discovered can be be passed into + * |-retrievePeripheralWithIdentifier:advertisementData:| for use as socket peripherals. If valid, + * the delegate will be notified. The peripheral must be advertising a service in + * |advertisedServiceUUIDs|. + * + * @param advertisedServiceUUID The service(s) the peripherals must be advertising. + */ +- (void)startNoScanModeWithAdvertisedServiceUUIDs: + (nullable NSArray<CBUUID *> *)advertisedServiceUUIDs; + +/** + * Tries to retrieve a peripheral that matches the given identifier, and, if successful, treats it + * as a newly found peripheral. + * + * @param identifier The peripheral identifier. + * @param advertisementData The advertisement data associated with the peripheral. + */ +- (void)retrievePeripheralWithIdentifier:(NSUUID *)identifier + advertisementData:(NSDictionary<NSString *, id> *)advertisementData; + +/** + * Stops "no-scan" mode. + */ +- (void)stopNoScanMode; + +/** + * Creates a GNSCentralPeerManager for an identifier. + * + * @param identifier Identifier from the idenfier. + * + * @return Central peer. + */ +- (nullable GNSCentralPeerManager *)retrieveCentralPeerWithIdentifier:(NSUUID *)identifier; + +@end + +NS_ASSUME_NONNULL_END diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.m b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.m new file mode 100644 index 00000000000..67ff7358909 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.m @@ -0,0 +1,372 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager+Private.h" + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager+Private.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.h" +#import "GoogleToolboxForMac/GTMLogger.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *CentralManagerStateString(CBCentralManagerState state) { + switch (state) { + case CBCentralManagerStateUnknown: + return @"CBCentralManagerStateUnknown"; + case CBCentralManagerStateResetting: + return @"CBCentralManagerStateResetting"; + case CBCentralManagerStateUnsupported: + return @"CBCentralManagerStateUnsupported"; + case CBCentralManagerStateUnauthorized: + return @"CBCentralManagerStateUnauthorized"; + case CBCentralManagerStatePoweredOff: + return @"CBCentralManagerStatePoweredOff"; + case CBCentralManagerStatePoweredOn: + return @"CBCentralManagerStatePoweredOn"; + } + return [NSString stringWithFormat:@"CBCentralManagerState Unknown(%ld)", (long)state]; +} + +@interface GNSCentralManager () { + NSString *_advertisedName; + CBUUID *_advertisedServiceUUID; + NSArray<CBUUID *> *_advertisedServiceUUIDs; + CBCentralManager *_cbCentralManager; + NSMapTable *_centralPeerManagers; + BOOL _cbCentralScanStarted; + dispatch_queue_t _queue; +} + +@end + +@implementation GNSCentralManager + ++ (CBCentralManager *)centralManagerWithDelegate:(id<CBCentralManagerDelegate>)delegate + queue:(nullable dispatch_queue_t)queue + options:(nullable NSDictionary<NSString *, id> *)options { + return [[CBCentralManager alloc] initWithDelegate:delegate queue:queue options:options]; +} + +- (instancetype)init { + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (nullable instancetype)initWithSocketServiceUUID:(CBUUID *)socketServiceUUID + queue:(dispatch_queue_t)queue { + NSAssert(socketServiceUUID, @"Cannot create a GNSCentralManager with nil service UUID."); + self = [super init]; + if (self) { + _socketServiceUUID = socketServiceUUID; + _queue = queue; + _cbCentralManager = [[self class] + centralManagerWithDelegate:self + queue:queue + options:@{CBCentralManagerOptionShowPowerAlertKey : @NO}]; + _centralPeerManagers = [NSMapTable strongToWeakObjectsMapTable]; + } + return self; +} + +- (nullable instancetype)initWithSocketServiceUUID:(CBUUID *)socketServiceUUID { + return [self initWithSocketServiceUUID:socketServiceUUID queue:dispatch_get_main_queue()]; +} + +- (void)dealloc { + _cbCentralManager.delegate = nil; +} + +- (void)startScanWithAdvertisedName:(nullable NSString *)advertisedName + advertisedServiceUUID:(nullable CBUUID *)advertisedServiceUUID { + NSAssert(_cbCentralManager, @"CBCentralManager not created."); + if (_scanning) { + return; + } + _advertisedServiceUUID = advertisedServiceUUID; + _advertisedName = advertisedName; + _scanning = YES; + if (_cbCentralManager.state == CBCentralManagerStatePoweredOn) { + [self startCBScan]; + } +} + +- (void)startScanWithAdvertisedServiceUUIDs:(nullable NSArray<CBUUID *> *)advertisedServiceUUIDs { + NSAssert(_cbCentralManager, @"CBCentralManager not created."); + if (_scanning) { + return; + } + [self startNoScanModeWithAdvertisedServiceUUIDs:_advertisedServiceUUIDs]; + _scanning = YES; + if (_cbCentralManager.state == CBCentralManagerStatePoweredOn) { + [self startCBScan]; + } +} + +- (void)stopScan { + if (!_scanning) { + return; + } + _advertisedName = nil; + if (_cbCentralScanStarted) { + [self stopCBScan]; + } + _scanning = NO; + [self stopNoScanMode]; +} + +- (void)startNoScanModeWithAdvertisedServiceUUIDs: + (nullable NSArray<CBUUID *> *)advertisedServiceUUIDs { + _advertisedServiceUUIDs = [advertisedServiceUUIDs copy]; +} + +- (void)retrievePeripheralWithIdentifier:(NSUUID *)identifier + advertisementData:(nonnull NSDictionary<NSString *, id> *)advertisementData { + NSArray<CBPeripheral *> *peripherals = + [_cbCentralManager retrievePeripheralsWithIdentifiers:@[identifier]]; + if (peripherals.count > 0) { + [self centralManager:_cbCentralManager + didDiscoverPeripheral:peripherals[0] + advertisementData:advertisementData + RSSI:@(127)]; // RSSI not available + } +} + +- (void)stopNoScanMode { + _advertisedServiceUUIDs = nil; +} + +- (nullable GNSCentralPeerManager *)retrieveCentralPeerWithIdentifier:(NSUUID *)identifier { + NSAssert(_cbCentralManager, @"CBCentralManager not created."); + NSAssert(identifier, @"Should have an identifier, self: %@", self); + GNSCentralPeerManager *peerManager = [_centralPeerManagers objectForKey:identifier]; + if (peerManager) { + GTMLoggerAssert(@"Previous GNSCentralPeerManager still alive, self: %@, peer manager: %@", self, + peerManager); + return nil; + } + NSArray<CBPeripheral *> *peripherals = + [_cbCentralManager retrievePeripheralsWithIdentifiers:@[ identifier ]]; + for (CBPeripheral *peripheral in peripherals) { + if ([peripheral.identifier isEqual:identifier]) { + peerManager = [self createCentralPeerManagerWithPeripheral:peripheral]; + [_centralPeerManagers setObject:peerManager forKey:identifier]; + return peerManager; + } + } + GTMLoggerError(@"CBPeripheral not found, self: %@, identifier %@, peripherals %@", self, + identifier, peripherals); + return nil; +} + +- (CBCentralManagerState)cbCentralManagerState { + // Cast to avoid some warnings in XCode 8. When Xcode 8 is the default + // swap CBCentralManagerState for CBManagerState. + return (CBCentralManagerState)_cbCentralManager.state; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p, socket service UUID: %@, advertised name: %@, " + @"scanning: %@, central scan started: %@, bluetooth state %@, " + @"CBCentralManager %@, peer managers: %@>", + NSStringFromClass([self class]), self, _socketServiceUUID, + _advertisedName, _scanning ? @"YES" : @"NO", + _cbCentralScanStarted ? @"YES" : @"NO", + CentralManagerStateString([self cbCentralManagerState]), + _cbCentralManager, _centralPeerManagers]; +} + +#pragma mark - Private + +- (void)startCBScan { + if (_cbCentralScanStarted) { + return; + } + _cbCentralScanStarted = YES; + // CBCentralManagerScanOptionAllowDuplicatesKey has to be set to YES. There are some cases where + // CoreBluetooth never discovers the phone if the setup mode on the phone is enable after opening + // the setup window on OS X. + // The drawback is the same CBPeripheral is discovered several times per second. + NSDictionary<NSString *, id> *options = @{CBCentralManagerScanOptionAllowDuplicatesKey : @YES}; + if (_advertisedName) { + GTMLoggerInfo(@"Start scanning for all peripherals. Filter peripherals with advertised " + "service UUID %@ or advertised name %@.", + [_socketServiceUUID UUIDString], _advertisedName); + [_cbCentralManager scanForPeripheralsWithServices:nil options:options]; + } else if (_advertisedServiceUUID) { + // Not logging this case to avoid spamming the logs. + [_cbCentralManager scanForPeripheralsWithServices:@[ _advertisedServiceUUID ] options:options]; + } else if (_advertisedServiceUUIDs) { + // Not logging this case to avoid spamming the logs. + [_cbCentralManager scanForPeripheralsWithServices:_advertisedServiceUUIDs options:options]; + } else { + GTMLoggerInfo(@"Start scanning for all peripherals."); + [_cbCentralManager scanForPeripheralsWithServices:nil options:options]; + } +} + +- (void)stopCBScan { + if (!_cbCentralScanStarted) { + return; + } + _cbCentralScanStarted = NO; + [_cbCentralManager stopScan]; +} + +- (void)peripheralDisconnected:(CBPeripheral *)peripheral withError:(NSError *)error { + NSAssert(peripheral.state == CBPeripheralStateDisconnected, + @"Peripheral should be disconnected %@", peripheral); + GNSCentralPeerManager *peerManager = [self centralPeerForPeripheral:peripheral]; + if (peerManager) { + [peerManager bleDisconnectedWithError:error]; + } +} + +- (void)connectPeripheralForPeer:(GNSCentralPeerManager *)peer + options:(nullable NSDictionary<NSString *, id> *)options { + GTMLoggerInfo(@"Connect peer %@ options %@", peer, options); + [_cbCentralManager connectPeripheral:peer.cbPeripheral options:options]; +} + +- (void)cancelPeripheralConnectionForPeer:(GNSCentralPeerManager *)peer { + GTMLoggerInfo(@"Cancel peer connection %@", peer); + [_cbCentralManager cancelPeripheralConnection:peer.cbPeripheral]; +} + +- (void)centralPeerManagerDidDisconnect:(GNSCentralPeerManager *)peer { + GTMLoggerInfo(@"Central manager removing central peer manager, central manager: %@, peer %@", + self, peer); + if (peer.cbPeripheral.state != CBPeripheralStateDisconnected) { + GTMLoggerInfo(@"Unexpected peripheral state %@", peer.cbPeripheral); + } +} + +- (CBCentralManager *)cbCentralManager { + return _cbCentralManager; +} + +- (GNSCentralPeerManager *)createCentralPeerManagerWithPeripheral:(CBPeripheral *)peripheral { + return [[GNSCentralPeerManager alloc] + initWithPeripheral:peripheral centralManager:self queue:_queue]; +} + +- (GNSCentralPeerManager *)centralPeerForPeripheral:(CBPeripheral *)peripheral { + NSParameterAssert(peripheral); + GNSCentralPeerManager *peerManager = [_centralPeerManagers objectForKey:peripheral.identifier]; + if (!peerManager) { + GTMLoggerDebug(@"No peer manager found for peripheral %@", peripheral); + return nil; + } + if (peerManager.cbPeripheral != peripheral) { + // There is a peer manager with a different peripheral object than |peripheral|, but with the + // same identifier. Something really wrong happened in CoreBluetooth here. + GTMLoggerError(@"Peer Manager %@ has a different peripheral than %@ [CentralManager = %@]", + peerManager, peripheral, self); + return nil; + } + return peerManager; +} + +#pragma mark - CBCentralManagerDelegate + +- (void)centralManagerDidUpdateState:(CBCentralManager *)central { + NSAssert(central == _cbCentralManager, @"Wrong peripheral manager."); + GTMLoggerInfo(@"CoreBluetooth state: %@", + CentralManagerStateString([self cbCentralManagerState])); + switch (central.state) { + case CBCentralManagerStatePoweredOn: { + if (_scanning) { + [self startCBScan]; + } + break; + } + case CBCentralManagerStatePoweredOff: + case CBCentralManagerStateResetting: + case CBCentralManagerStateUnauthorized: + case CBCentralManagerStateUnsupported: + case CBCentralManagerStateUnknown: + [self stopCBScan]; + break; + } + [_delegate centralManagerDidUpdateBleState:self]; + for (GNSCentralPeerManager *peerManager in _centralPeerManagers.objectEnumerator) { + [peerManager cbCentralManagerStateDidUpdate]; + } +} + +- (void)centralManager:(CBCentralManager *)central + didDiscoverPeripheral:(CBPeripheral *)peripheral + advertisementData:(NSDictionary<NSString *, id> *)advertisementData + RSSI:(NSNumber *)RSSI { + // This method can be called several times per second since + // CBCentralManagerScanOptionAllowDuplicatesKey is set to YES. Therefore, don't log. + NSArray<CBUUID *> *advertisedServiceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey]; + NSString *advertisedName = advertisementData[CBAdvertisementDataLocalNameKey]; + BOOL noAdvertisementFilter = !_advertisedServiceUUID && !_advertisedName; + if (noAdvertisementFilter || [advertisedServiceUUIDs containsObject:_advertisedServiceUUID] || + [_advertisedName isEqualToString:advertisedName]) { + NSUUID *identifier = peripheral.identifier; + GNSCentralPeerManager *peerManager = [_centralPeerManagers objectForKey:identifier]; + if (!peerManager) { + GTMLoggerInfo(@"Discovered peer peripheral %@", peripheral); + peerManager = [self createCentralPeerManagerWithPeripheral:peripheral]; + [_centralPeerManagers setObject:peerManager forKey:identifier]; + [_delegate centralManager:self + didDiscoverPeer:peerManager + advertisementData:advertisementData]; + } + } +} + +- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral { + NSAssert(central == _cbCentralManager, @"Unexpected central manager"); + GTMLoggerInfo(@"Connected to %@", peripheral); + GNSCentralPeerManager *peerManager = [self centralPeerForPeripheral:peripheral]; + if (!peerManager) { + GTMLoggerError( + @"No peer manager found for connected peripheral %@. Cancel peripheral connection", + peripheral); + [_cbCentralManager cancelPeripheralConnection:peripheral]; + return; + } + [peerManager bleConnected]; +} + +- (void)centralManager:(CBCentralManager *)central + didFailToConnectPeripheral:(CBPeripheral *)peripheral + error:(nullable NSError *)error { + NSAssert(central == _cbCentralManager, @"Unexpected central manager"); + GTMLoggerInfo(@"Fail to connect to %@, error %@", peripheral, error); + [self peripheralDisconnected:peripheral withError:error]; +} + +- (void)centralManager:(CBCentralManager *)central + didDisconnectPeripheral:(CBPeripheral *)peripheral + error:(nullable NSError *)error { + NSAssert(central == _cbCentralManager, @"Unexpected central manager"); + GTMLoggerInfo(@"Did disconnect %@, error %@", peripheral, error); + [self peripheralDisconnected:peripheral withError:error]; +} + +@end + +@implementation GNSCentralManager (VisibleForTesting) + +- (CBCentralManager *)testing_cbCentralManager { + return _cbCentralManager; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager+Private.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager+Private.h new file mode 100644 index 00000000000..b99a572b24c --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager+Private.h @@ -0,0 +1,73 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.h" + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h" + +NS_ASSUME_NONNULL_BEGIN + +@class CBPeripheral; +@class GNSCentralManager; + +@interface GNSCentralPeerManager ()<GNSSocketOwner, CBPeripheralDelegate> + +@property(nonatomic, readonly) CBPeripheral *cbPeripheral; + +/** + * Creates a new instance of GNSCentralPeerManager. Should only be used by GNSCentralManager. + * + * @param peripheral CoreBluetooth peripheral instance + * @param centralManager Central manager + * @param queue The queue this object is called on and callbacks are made on. + * + * @return GNSCentralPeerManager instance. + */ +- (nullable instancetype)initWithPeripheral:(CBPeripheral *)peripheral + centralManager:(GNSCentralManager *)centralManager + queue:(dispatch_queue_t)queue + NS_DESIGNATED_INITIALIZER; + +/** + * Creates a new instance of GNSCentralPeerManager using the main queue for callbacks. + */ +- (nullable instancetype)initWithPeripheral:(CBPeripheral *)peripheral + centralManager:(GNSCentralManager *)centralManager; + +/** + * Called by the central manager when the peripheral is connected with BLE. + */ +- (void)bleConnected; + +/** + * Called by the central manager when the peripheral is disconnected from BLE. + * + * @param error Error while being disconnected. + */ +- (void)bleDisconnectedWithError:(nullable NSError *)error; + +/** + * Called by the central manager when the bluetooth state changes. + */ +- (void)cbCentralManagerStateDidUpdate; + +@end + +@interface GNSCentralPeerManager (TestingHelpers) + +- (NSTimer *)testing_connectionConfirmTimer; + +@end + +NS_ASSUME_NONNULL_END diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.h new file mode 100644 index 00000000000..8f640409228 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.h @@ -0,0 +1,99 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import <Foundation/Foundation.h> + +@class CBUUID; +@class GNSCentralPeerManager; +@class GNSSocket; + +typedef void (^GNSCentralSocketCompletion)(GNSSocket *socket, NSError *error); +typedef void (^GNSPairingCompletion)(BOOL pairing, NSError *error); +typedef void (^GNSReadRRSIValueCompletion)(NSNumber *rssi, NSError *error); + +/** + * This class manage one CBPeripheral. It can only manage one service. The service UUID is set + * by GNSCentralManager. The GNSCentralPeerManager instance is created by GNSCentralManager. + * From this class a socket can be created using -[GNSCentralPeerManager socketWithCompletion:]. + * See GNSCentralManager header for more information. + * + * This class is not thread-safe. + */ +@interface GNSCentralPeerManager : NSObject + +/** + * MTU value. The default is 100. + */ +@property(nonatomic) NSUInteger socketMaximumUpdateValueLength; + +/** + * Peer identifier. + */ +@property(nonatomic, readonly) NSUUID *identifier; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Creates the socket. When the socket is created and ready to use, |completion| is called. |data| + * is sent during the connection handshake, and is going to be treated as the first message on the + * peripheral/server, as established by the Weave BLE protocol. |data| can contain at most 13 bytes. + * + * If the socket fails to be created |completion| is called with an error. This method should be + * called once until the completion is called, otherwise |completion| will be called with an + * operation in progress error. + * + * @param handshakeData Data sent during the connection handshake. Should contain at most 13 bytes. + * @param pairingCharacteristic If YES, peripheral need to have the pairing characteristic in order + * to get the socket. The pairing characteristic is required to trigger the pairing. + * @param completion Handler called when the socket is created or failed to be created. It is + * guaranteed that either the socket or the error passed to |completion| are not nil. + */ +- (void)socketWithHandshakeData:(NSData *)handshakeData + pairingCharacteristic:(BOOL)hasPairingCharacteristic + completion:(GNSCentralSocketCompletion)completion; + +/** + * See -[GNSCentralPeerManager socketWithHandshakeData:pairingCharacteristic:completion:]. + */ +- (void)socketWithPairingCharacteristic:(BOOL)shouldAddPairingCharacteristics + completion:(GNSCentralSocketCompletion)completion; + +/** + * Cancel the pending socket request, if any. |completion| (passed in + * |socketWithPairingCharacteristic:completion:|) will be called (with a nil socket) when the + * pending socket request is successfully cancelled (and disconnected). + * + * Note that, after calling |cancelPendingSocket| is still necessary to wait for the |completion| + * (passed in a previous |socketWithPairingCharacteristic:completion:| invocation) to be called + * before calling |socketWithPairingCharacteristic:completion:| again. + */ +- (void)cancelPendingSocket; + +/** + * Triggers pairing between the central and the peripheral. The socket must have been generated + * with pairing characteristic. The completion has to be called before calling this method again. + * + * @param completion Completion called when the pairing has failed or succeed. + */ +- (void)startBluetoothPairingWithCompletion:(GNSPairingCompletion)completion; + +/** + * Retrieve the RSSI value while the central peer is connected. If a completion is pending while + * this method is called again, the second completion will be added. + * + * @param completion Called when the value is available. Cannot be nil. + */ +- (void)readRSSIWithCompletion:(GNSReadRRSIValueCompletion)completion; + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.m b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.m new file mode 100644 index 00000000000..92cb2a36b0f --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.m @@ -0,0 +1,766 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager+Private.h" + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager+Private.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils+Private.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSWeavePacket.h" +#import "GoogleToolboxForMac/GTMLogger.h" + +static const NSTimeInterval kMaxConnectionConfirmWaitTimeInSeconds = 2; +static const NSTimeInterval kPeripheralFailedToConnectTimeout = 5.0; +static const NSTimeInterval kChracteristicWriteTimeoutInSeconds = 0.5; + +// The Weave BLE protocol has only one valid version. +static const UInt16 kWeaveVersionSupported = 1; + +typedef NS_ENUM(NSInteger, GNSCentralPeerManagerState) { + GNSCentralPeerManagerStateNotConnected = 0, + GNSCentralPeerManagerStateBleConnecting = 1, + GNSCentralPeerManagerStateBleConnected = 2, + GNSCentralPeerManagerStateDiscoveringService = 3, + GNSCentralPeerManagerStateDiscoveringCharacteristic = 4, + GNSCentralPeerManagerStateSettingNotifications = 5, + GNSCentralPeerManagerStateSocketCommunication = 6, + // "BLEDisconnecting" is optional. If CoreBluetooth notifies that the central is disconnected, + // the state will switch from any state to "NotConnected" without going through + // "BLEDisconnecting". + GNSCentralPeerManagerStateBleDisconnecting = 7, +}; + +typedef void (^GNSCentralPeerManagerDiscoverCharacteristicsCallback)(NSError *error); + +static NSUInteger gGNSCentralPeerManagerMaximumUpdateValue = 100; + +static NSString *StateDescription(GNSCentralPeerManagerState state) { + switch (state) { + case GNSCentralPeerManagerStateNotConnected: + return @"Not connected"; + case GNSCentralPeerManagerStateBleConnecting: + return @"BLE Connecting"; + case GNSCentralPeerManagerStateBleConnected: + return @"BLE Connected"; + case GNSCentralPeerManagerStateDiscoveringService: + return @"Discovering service"; + case GNSCentralPeerManagerStateDiscoveringCharacteristic: + return @"Discovering characteristics"; + case GNSCentralPeerManagerStateSettingNotifications: + return @"Setting characteristic notification"; + case GNSCentralPeerManagerStateSocketCommunication: + return @"Socket communication"; + case GNSCentralPeerManagerStateBleDisconnecting: + return @"BLE Disconnecting"; + } + return @"Unknown"; +} + +static NSString *PeripheralStateString(CBPeripheralState state) { + switch (state) { + case CBPeripheralStateDisconnected: + return @"CBPeripheralStateDisconnected"; + case CBPeripheralStateConnecting: + return @"CBPeripheralStateConnecting"; + case CBPeripheralStateConnected: + return @"CBPeripheralStateConnected"; + case CBPeripheralStateDisconnecting: + return @"CBPeripheralStateDisconnecting"; + } + return [NSString stringWithFormat:@"CBPeripheralState Unknown (%ld)", (long)state]; +} + +@interface GNSCentralPeerManager ()<GNSWeavePacketHandler> +@property(nonatomic) GNSCentralPeerManagerState state; +@property(nonatomic) GNSCentralManager *centralManager; +@property(nonatomic) dispatch_queue_t queue; +@property(nonatomic) int indexOfServiceToCheck; +@property(nonatomic) CBService *socketService; +@property(nonatomic) CBCharacteristic *outgoingChar; +@property(nonatomic) CBCharacteristic *incomingChar; +@property(nonatomic) CBCharacteristic *pairingChar; +@property(nonatomic, weak) GNSSocket *socket; +@property(nonatomic) GNSCentralSocketCompletion discoveringServiceSocketCompletion; +@property(nonatomic) BOOL shouldAddPairingCharacteristic; +@property(nonatomic) GNSPairingCompletion pairingCompletion; +@property(nonatomic) NSMutableArray<GNSReadRRSIValueCompletion> *readRSSIValueCompletions; +@property(nonatomic) NSError *disconnectedError; +@property(nonatomic) NSData *connectionRequestData; +@property(nonatomic) NSDate *startConnectionTime; + +// This timer is scheduled right after the connection request packet is sent, and fires if no +// connection confirm packet is received before |kMaxConnectionConfirmWaitTimeInSeconds|. +@property(nonatomic) NSTimer *connectionConfirmTimer; + +// The completion of a write-with-response to peripheral; nil if none is in progress. +@property(nonatomic) GNSErrorHandler dataWriteCompletion; +@end + +@implementation GNSCentralPeerManager + +@synthesize cbPeripheral = _cbPeripheral; + +// Used for testing. A test subclass can override this method so a mock timer can be returned. ++ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval + target:(id)target + selector:(SEL)selector + userInfo:(nullable id)userInfo + repeats:(BOOL)yesOrNo { + return [NSTimer scheduledTimerWithTimeInterval:timeInterval + target:target + selector:selector + userInfo:userInfo + repeats:yesOrNo]; +} + +- (instancetype)initWithPeripheral:(CBPeripheral *)peripheral + centralManager:(GNSCentralManager *)centralManager + queue:(dispatch_queue_t)queue { + self = [super init]; + if (self) { + _cbPeripheral = peripheral; + _cbPeripheral.delegate = self; + _centralManager = centralManager; + _queue = queue; + _socketMaximumUpdateValueLength = gGNSCentralPeerManagerMaximumUpdateValue; + _state = GNSCentralPeerManagerStateNotConnected; + GTMLoggerInfo(@"Peripheral %@", _cbPeripheral); + } + return self; +} + +- (instancetype)initWithPeripheral:(CBPeripheral *)peripheral + centralManager:(GNSCentralManager *)centralManager { + return [self initWithPeripheral:peripheral + centralManager:centralManager + queue:dispatch_get_main_queue()]; +} + +- (void)dealloc { + GTMLoggerDebug(@"Dealloc CentralPeerManager with _cbPeripheral %@", _cbPeripheral); + _cbPeripheral.delegate = nil; + if (_cbPeripheral.state != CBPeripheralStateDisconnected) { + [_centralManager cancelPeripheralConnectionForPeer:self]; + } +} + +- (void)socketWithHandshakeData:(NSData *)handshakeData + pairingCharacteristic:(BOOL)hasPairingCharacteristic + completion:(GNSCentralSocketCompletion)completion { + NSAssert(handshakeData.length <= kGNSMaxCentralHandshakeDataSize, @"Handshake data is too large"); + _connectionRequestData = handshakeData; + [self socketWithPairingCharacteristic:hasPairingCharacteristic completion:completion]; +} + +- (void)socketWithPairingCharacteristic:(BOOL)shouldAddPairingCharacteristics + completion:(GNSCentralSocketCompletion)completion { + GTMLoggerInfo(@"Request socket %@", self); + if (_state != GNSCentralPeerManagerStateNotConnected) { + GTMLoggerInfo(@"There is a pending socket request"); + if (completion) { + dispatch_async(_queue, ^{ + completion(nil, GNSErrorWithCode(GNSErrorOperationInProgress)); + }); + } + return; + } + _shouldAddPairingCharacteristic = shouldAddPairingCharacteristics; + _discoveringServiceSocketCompletion = completion; + [self bleConnect]; +} + +- (void)cancelPendingSocket { + if (_discoveringServiceSocketCompletion == nil) { + GTMLoggerInfo(@"No pending socket, current socket: %@", _socket); + return; + } + GTMLoggerInfo(@"Cancelling pending socket"); + NSError *cancelPendingSocketRequested = GNSErrorWithCode(GNSErrorCancelPendingSocketRequested); + [self disconnectingWithError:cancelPendingSocketRequested]; +} + +- (void)startBluetoothPairingWithCompletion:(GNSPairingCompletion)completion { + NSAssert(_socket, @"Peer socket should have been created."); + NSAssert(!_pairingCompletion, @"Pairing already pending."); + NSAssert(_pairingChar, @"No a pairing characteristic available."); + _pairingCompletion = completion; + [_cbPeripheral readValueForCharacteristic:_pairingChar]; +} + +- (NSUUID *)identifier { + return _cbPeripheral.identifier; +} + +- (void)readRSSIWithCompletion:(GNSReadRRSIValueCompletion)completion { + NSAssert(completion, @"Completion cannot be nil"); + if (![self isBLEConnected]) { + NSAssert(!_readRSSIValueCompletions, @"Should not have pending RSSI completions."); + dispatch_async(_queue, ^{ completion(nil, GNSErrorWithCode(GNSErrorNoConnection)); }); + return; + } + if (!_readRSSIValueCompletions) { + [_cbPeripheral readRSSI]; + _readRSSIValueCompletions = [NSMutableArray array]; + } + [_readRSSIValueCompletions addObject:completion]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p, state: \"%@\", peripheral identifier: %@, " + @"peripheral state: %@, peripheral: %@>", + [self class], self, StateDescription(_state), + _cbPeripheral.identifier.UUIDString, + PeripheralStateString(_cbPeripheral.state), _cbPeripheral]; +} + +#pragma mark - Private + +- (void)bleConnect { + GTMLoggerInfo(@"BLE connection started."); + _startConnectionTime = [NSDate date]; + + _state = GNSCentralPeerManagerStateBleConnecting; + if (_centralManager.cbCentralManagerState == CBCentralManagerStatePoweredOn) { + NSDictionary<NSString *, id> *options = @{ + CBConnectPeripheralOptionNotifyOnDisconnectionKey : @YES, +#if TARGET_OS_IPHONE + CBConnectPeripheralOptionNotifyOnConnectionKey : @YES, + CBConnectPeripheralOptionNotifyOnNotificationKey : @YES, +#endif + }; + [_centralManager connectPeripheralForPeer:self options:options]; + + // Workaround for an apparent Core Bluetooth problem: Either -didConnectPeripheral or + // -didFailToConnectPeripheral should be called at this point, but sometimes neither is called. + // In this case, report a timeout error. + __weak __typeof__(self) weakSelf = self; + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kPeripheralFailedToConnectTimeout * NSEC_PER_SEC)), + _queue, ^{ + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) return; + if (strongSelf.state == GNSCentralPeerManagerStateBleConnecting) { + GTMLoggerInfo(@"Timed out trying to connect to peripheral: %@", strongSelf.cbPeripheral); + [strongSelf.centralManager cancelPeripheralConnectionForPeer:self]; + [strongSelf bleDisconnectedWithError:GNSErrorWithCode(GNSErrorConnectionTimedOut)]; + } + }); + } +} + +- (void)bleConnected { + if (_cbPeripheral.state != CBPeripheralStateConnected) { + GTMLoggerInfo(@"Unexpected peripheral state: %ld", (long)_cbPeripheral.state); + } + GTMLoggerInfo(@"BLE connected. Elapsed time: %f", + [[NSDate date] timeIntervalSinceDate:_startConnectionTime]); + if (_state != GNSCentralPeerManagerStateBleConnecting) { + GTMLoggerInfo(@"Should not be connected with %@ when in %@ state", _cbPeripheral, + StateDescription(_state)); + return; + } + GTMLoggerInfo(@"BLE connected, peripheral: %@", _cbPeripheral); + [self discoverService]; +} + +- (void)bleDisconnectedWithError:(NSError *)error { + GTMLoggerInfo(@"BLE disconnected, peripheral: %@", _cbPeripheral); + if (_state == GNSCentralPeerManagerStateNotConnected) { + return; + } else { + if (!_disconnectedError) { + _disconnectedError = error; + } + [_connectionConfirmTimer invalidate]; + _connectionConfirmTimer = nil; + [self bleDisconnected]; + } +} + +- (void)cbCentralManagerStateDidUpdate { + if (_centralManager.cbCentralManagerState == CBCentralManagerStatePoweredOn && + _state == GNSCentralPeerManagerStateBleConnecting) { + [self bleConnect]; + } +} + +- (void)disconnectingWithError:(NSError *)error { + if (error) { + GTMLoggerInfo(@"Disconnected with error: %@, peripheral: %@", error, _cbPeripheral); + } else { + GTMLoggerInfo(@"Disconnected from peripheral: %@", _cbPeripheral); + } + [_connectionConfirmTimer invalidate]; + _connectionConfirmTimer = nil; + _connectionRequestData = nil; + _disconnectedError = error; + + // If there is a pending characteristic write, call the completion. + [self callDataWriteCompletionWithError:GNSErrorWithCode(GNSErrorLostConnection)]; + + [self cleanRSSICompletionAfterDisconnectionWithError:error]; + _state = GNSCentralPeerManagerStateBleDisconnecting; + if (_cbPeripheral.state != CBPeripheralStateDisconnected) { + [_centralManager cancelPeripheralConnectionForPeer:self]; + } else { + [self bleDisconnected]; + } +} + +- (void)bleDisconnected { + GTMLoggerInfo(@"BLE Disconnected %@", _cbPeripheral); + if (_cbPeripheral.state != CBPeripheralStateDisconnected) { + GTMLoggerInfo(@"Unexpected peripheral state: %ld", (long)_cbPeripheral.state); + } + NSAssert(_cbPeripheral.delegate == self, @"Self = %@ should be the delegate of %@", self, + _cbPeripheral); + + // Clean-up this peer manager. + [_centralManager centralPeerManagerDidDisconnect:self]; + GNSSocket *currentSocket = _socket; + _socketService = nil; + _socket = nil; + _state = GNSCentralPeerManagerStateNotConnected; + + // Inform all interested parties that the peripheral is disconnected. + [self cleanRSSICompletionAfterDisconnectionWithError:_disconnectedError]; + if (_discoveringServiceSocketCompletion) { + NSAssert(!_socket, @"Should not have a socket yet."); + if (!_disconnectedError) { + _disconnectedError = GNSErrorWithCode(GNSErrorNoConnection); + } + GNSCentralSocketCompletion completion = _discoveringServiceSocketCompletion; + _discoveringServiceSocketCompletion = nil; + dispatch_async(_queue, ^{ completion(nil, _disconnectedError); }); + } else if (currentSocket) { + [currentSocket didDisconnectWithError:_disconnectedError]; + } +} + +- (void)discoverService { + _state = GNSCentralPeerManagerStateDiscoveringService; + CBUUID *serviceUUID = _centralManager.socketServiceUUID; + GTMLoggerInfo(@"Discover service %@ on peripheral: %@", serviceUUID, _cbPeripheral); + NSArray<CBUUID *> *servicesToDiscover = @[ serviceUUID ]; + [_cbPeripheral discoverServices:servicesToDiscover]; +} + +- (void)discoverCharacteristics { + if (_cbPeripheral.services.count == 0) return; + GTMLoggerInfo(@"Discover characteristics, peer manager %@", self); + NSMutableArray<CBUUID *> *characteristics = [NSMutableArray + arrayWithObjects:[CBUUID UUIDWithString:kGNSWeaveToPeripheralCharUUIDString], + [CBUUID UUIDWithString:kGNSWeaveFromPeripheralCharUUIDString], nil]; + if (_shouldAddPairingCharacteristic) { + [characteristics addObject:[CBUUID UUIDWithString:kGNSPairingCharUUIDString]]; + } + [_cbPeripheral discoverCharacteristics:characteristics + forService:_cbPeripheral.services[_indexOfServiceToCheck]]; +} + +- (BOOL)findCharacteristicsFromService:(CBService *)service { + CBCharacteristic *outgoingChar = nil; + CBCharacteristic *incomingChar = nil; + CBCharacteristic *pairingChar = nil; + for (CBCharacteristic *characteristic in service.characteristics) { + if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kGNSWeaveToPeripheralCharUUIDString]]) { + outgoingChar = characteristic; + } else if ([characteristic.UUID + isEqual:[CBUUID UUIDWithString:kGNSWeaveFromPeripheralCharUUIDString]]) { + incomingChar = characteristic; + } else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kGNSPairingCharUUIDString]]) { + pairingChar = characteristic; + } + } + if (!outgoingChar || !incomingChar || (_shouldAddPairingCharacteristic && !pairingChar)) { + return NO; + } + _incomingChar = incomingChar; + _outgoingChar = outgoingChar; + _pairingChar = pairingChar; + return YES; +} + +- (void)setCharacteristicNotification { + NSAssert(_incomingChar, @"Should have found incoming characteristic"); + _state = GNSCentralPeerManagerStateSettingNotifications; + [_cbPeripheral setNotifyValue:YES forCharacteristic:_incomingChar]; +} + +- (CBPeripheral *)cbPeripheral { + return _cbPeripheral; +} + +// If error is nil, kGNSNoConnection |error| is passed to the rssi completion. +- (void)cleanRSSICompletionAfterDisconnectionWithError:(NSError *)error { + if (_readRSSIValueCompletions) { + NSError *rssiCompletionError = error; + if (rssiCompletionError) { + rssiCompletionError = GNSErrorWithCode(GNSErrorNoConnection); + } + [self callRSSICompletionWithRSSIValue:nil error:rssiCompletionError]; + } +} + +- (void)callRSSICompletionWithRSSIValue:(NSNumber *)rssiValue error:(NSError *)error { + NSArray<GNSReadRRSIValueCompletion> *completions = _readRSSIValueCompletions; + _readRSSIValueCompletions = nil; + for (GNSReadRRSIValueCompletion completion in completions) { + dispatch_async(_queue, ^{ completion(rssiValue, error); }); + } +} + +- (BOOL)isBLEConnected { + switch (_state) { + case GNSCentralPeerManagerStateNotConnected: + case GNSCentralPeerManagerStateBleConnecting: + return NO; + case GNSCentralPeerManagerStateBleConnected: + case GNSCentralPeerManagerStateDiscoveringService: + case GNSCentralPeerManagerStateDiscoveringCharacteristic: + case GNSCentralPeerManagerStateSettingNotifications: + case GNSCentralPeerManagerStateSocketCommunication: + case GNSCentralPeerManagerStateBleDisconnecting: + return YES; + } +} + +- (void)timeOutConnectionForTimer:(NSTimer *)timer { + NSAssert(_state == GNSCentralPeerManagerStateSocketCommunication, + @"Timer (%@) fired on the wrong state. [self = %@]", _connectionConfirmTimer, self); + GTMLoggerInfo(@"Timing out %@ socket connection [self = %@].", _socket, self); + _connectionConfirmTimer = nil; + NSError *error = GNSErrorWithCode(GNSErrorConnectionTimedOut); + [self disconnectingWithError:error]; +} + +- (void)callDataWriteCompletionWithError:(NSError *)error { + if (_dataWriteCompletion) { + GNSErrorHandler dataWriteCompletion = _dataWriteCompletion; // tail call for reentrancy + _dataWriteCompletion = nil; + dispatch_async(_queue, ^{ dataWriteCompletion(error); }); + } +} + +#pragma mark - CBPeripheralDelegate + +- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error { + GTMLoggerInfo(@"BLE services discovered. Elapsed time: %f", + [[NSDate date] timeIntervalSinceDate:_startConnectionTime]); + if (_state != GNSCentralPeerManagerStateDiscoveringService) { + GTMLoggerInfo(@"Ignoring services discovered from %@ when in %@ state", _cbPeripheral, + StateDescription(_state)); + return; + } + GTMLoggerInfo(@"Service discovered for %@, error: %@", self, error); + if (error) { + [self disconnectingWithError:error]; + return; + } + + // Multiple services may have the same UUID, so find the socket service by checking for the + // expected characteristics. + _indexOfServiceToCheck = 0; + _state = GNSCentralPeerManagerStateDiscoveringCharacteristic; + [self discoverCharacteristics]; +} + +- (void)peripheral:(CBPeripheral *)peripheral + didDiscoverCharacteristicsForService:(CBService *)service + error:(NSError *)error { + GTMLoggerInfo(@"BLE characteristics discovered. Elapsed time: %f", + [[NSDate date] timeIntervalSinceDate:_startConnectionTime]); + if (_state != GNSCentralPeerManagerStateDiscoveringCharacteristic) { + GTMLoggerInfo(@"Ignoring characteristics discovered from %@ when in %@ state", _cbPeripheral, + StateDescription(_state)); + return; + } + GTMLoggerInfo(@"Characteristics discovered for service: %@, error: %@", service, error); + CBService *serviceBeingChecked = _cbPeripheral.services[_indexOfServiceToCheck]; + NSAssert([service.UUID isEqual:serviceBeingChecked.UUID], @"Unknown service %@ %@", + service, serviceBeingChecked.UUID); + if (error) { + [self disconnectingWithError:error]; + return; + } + if (![self findCharacteristicsFromService:serviceBeingChecked]) { + if (++_indexOfServiceToCheck == _cbPeripheral.services.count) { + // Didn't find the characteristics in any of the services. + NSError *missingCharacteristicError = GNSErrorWithCode(GNSErrorMissingCharacteristics); + [self disconnectingWithError:missingCharacteristicError]; + } else { + // Check the next service in the list. + [self discoverCharacteristics]; + } + return; + } + [self setCharacteristicNotification]; +} + +- (void)handleWeaveError:(GNSError)errorCode socket:(GNSSocket *)socket { + if (!socket) { + return; + } + NSAssert(socket == _socket, @"Wrong socket, socket = %@, _socket = %@.", socket, _socket); + NSError *error = GNSErrorWithCode(errorCode); + // Only send the error if we didn't already received an error packet. + if (errorCode != GNSErrorWeaveErrorPacketReceived) { + GNSWeaveErrorPacket *errorPacket = + [[GNSWeaveErrorPacket alloc] initWithPacketCounter:socket.sendPacketCounter]; + [self sendPacket:errorPacket]; + } + [self disconnectingWithError:error]; +} + +- (void)peripheral:(CBPeripheral *)peripheral + didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic + error:(NSError *)error { + if (_state != GNSCentralPeerManagerStateSocketCommunication) { + GTMLoggerInfo(@"Ignoring data received from %@ when in %@ state", _cbPeripheral, + StateDescription(_state)); + return; + } + NSAssert(peripheral == _cbPeripheral, @"Unknown peripheral."); + if ([characteristic.UUID isEqual:_pairingChar.UUID]) { + GNSPairingCompletion completion = _pairingCompletion; + _pairingCompletion = nil; + if (completion) dispatch_async(_queue, ^{ completion(error == nil, error); }); + return; + } + NSAssert(characteristic == _incomingChar, @"Should read only from the incoming characteristic."); + NSAssert(_socket, @"Should have a socket in the %@ state", StateDescription(_state)); + NSData *charValue = characteristic.value; + // Truncate the packet to expected size: |kGNSMinSupportedPacketSize| for the first packet and + // |socket.packetSize| for the rest. + NSUInteger truncatedSize = MIN(_socket.packetSize, charValue.length); + if (truncatedSize != charValue.length) { + GTMLoggerInfo(@"Packet with %ld bytes trucanted to %ld", (long)charValue.length, + (long)truncatedSize); + } + NSData *packetData = [charValue subdataWithRange:NSMakeRange(0, truncatedSize)]; + NSError *parsingError = nil; + GNSWeavePacket *packet = [GNSWeavePacket parseData:packetData error:&parsingError]; + if (!packet) { + GTMLoggerError(@"Error parsing Weave packet (error = %@).", parsingError); + [self handleWeaveError:GNSErrorParsingWeavePacket socket:_socket]; + return; + } + if (packet.packetCounter != _socket.receivePacketCounter) { + GTMLoggerError(@"Wrong packet counter, [received %d, expected %d].", packet.packetCounter, + _socket.receivePacketCounter); + [self handleWeaveError:GNSErrorWrongWeavePacketCounter socket:_socket]; + return; + } + [packet visitWithHandler:self context:nil]; + [_socket incrementReceivePacketCounter]; +} + +- (void)peripheral:(CBPeripheral *)peripheral + didWriteValueForCharacteristic:(CBCharacteristic *)characteristic + error:(NSError *)error { + // Note: Avoid using |characteristic.value| here as it seems it is always nil. + if (error) { + GTMLoggerInfo(@"Characteristic write failed with error: %@", error); + [self disconnectingWithError:error]; + } else { + GTMLoggerInfo(@"Characteristic write succeeded"); + } + [self callDataWriteCompletionWithError:error]; +} + +// This method sends |packet| fitting a single characteristic write to |socket|. All packets sent by +// this class (not the socket) must use this method. +- (void)sendPacket:(GNSWeavePacket *)packet { + GTMLoggerInfo(@"Writing value to characteristic (internal)"); + NSAssert(packet.packetCounter == _socket.sendPacketCounter, @"Wrong packet counter."); + [_cbPeripheral writeValue:[packet serialize] + forCharacteristic:_outgoingChar + type:CBCharacteristicWriteWithResponse]; + [_socket incrementSendPacketCounter]; +} + +- (void)peripheral:(CBPeripheral *)peripheral + didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic + error:(NSError *)error { + GTMLoggerInfo(@"BLE characteristic notifications started. Elapsed time: %f", + [[NSDate date] timeIntervalSinceDate:_startConnectionTime]); + if (_state != GNSCentralPeerManagerStateSettingNotifications) { + GTMLoggerInfo(@"Ignoring notification status change for %@ when in %@ state", _cbPeripheral, + StateDescription(_state)); + return; + } + GTMLoggerInfo(@"Characteristic notification update: %@, error: %@", characteristic, error); + if (error) { + [self disconnectingWithError:error]; + return; + } + NSAssert([characteristic.UUID isEqual:_incomingChar.UUID], @"Wrong characteristic"); + NSAssert(_state == GNSCentralPeerManagerStateSettingNotifications, @"Wrong state"); + _state = GNSCentralPeerManagerStateSocketCommunication; + GTMLoggerInfo(@"Socket ready %@", self); + GNSSocket *socket = + [[GNSSocket alloc] initWithOwner:self peripheralPeer:_cbPeripheral queue:_queue]; + GNSCentralSocketCompletion completion = _discoveringServiceSocketCompletion; + _discoveringServiceSocketCompletion = nil; + _socket = socket; + dispatch_async(_queue, ^{ completion(socket, nil); }); + if (!_socket) { + [self disconnectingWithError:nil]; + return; + } + GTMLoggerInfo(@"Sending connection request packet."); + // On iOS/OS X the central (client) doesn't have access to the negotiated BLE connection MTU. So, + // according to the Weave BLE protocol specs, it should send 0. The peripheral will then choose + // the appropriated value for the packet size and send it back on the connection confirm packet. + GNSWeaveConnectionRequestPacket *connectionRequest = + [[GNSWeaveConnectionRequestPacket alloc] initWithMinVersion:kWeaveVersionSupported + maxVersion:kWeaveVersionSupported + maxPacketSize:0 + data:_connectionRequestData]; + [self sendPacket:connectionRequest]; + _connectionConfirmTimer = + [[self class] scheduledTimerWithTimeInterval:kMaxConnectionConfirmWaitTimeInSeconds + target:self + selector:@selector(timeOutConnectionForTimer:) + userInfo:nil + repeats:NO]; +} + +- (void)peripheral:(CBPeripheral *)peripheral didReadRSSI:(NSNumber *)RSSI error:(NSError *)error { + if (error) { + GTMLoggerError(@"Error to read RSSI %@", error); + } + [self callRSSICompletionWithRSSIValue:RSSI error:error]; +} + +#pragma mark - GNSSocketOwner + +- (NSUInteger)socketMaximumUpdateValueLength:(GNSSocket *)socket { + return _socketMaximumUpdateValueLength; +} + +- (void)sendData:(NSData *)data socket:(GNSSocket *)socket completion:(GNSErrorHandler)completion { + GTMLoggerInfo(@"Writing value to characteristic"); + if (_dataWriteCompletion != nil) { + // This shouldn't happen because writes should be serialized by the socket code. But log it + // in case there's a bug that causes it to happen. + GTMLoggerInfo(@"Previous characteristic data write didn't complete"); + } + + // Sometimes -didWriteValueForCharacteristic: isn't called, leaving the write operation hanging. + // If it times out, call the completion with an error. + __weak __typeof__(self) weakSelf = self; + dispatch_block_t dataWriteTimeoutBlock = dispatch_block_create(0, ^{ + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf || !socket.isConnected) return; + GTMLoggerInfo(@"Characteristic data write timed out"); + [strongSelf callDataWriteCompletionWithError:GNSErrorWithCode(GNSErrorConnectionTimedOut)]; + }); + _dataWriteCompletion = ^(NSError *error) { + // -didWriteValueForCharacteristic: was called, so cancel the timeout + dispatch_block_cancel(dataWriteTimeoutBlock); + completion(error); + }; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(kChracteristicWriteTimeoutInSeconds * NSEC_PER_SEC)), + _queue, + dataWriteTimeoutBlock); + + [_cbPeripheral writeValue:data + forCharacteristic:_outgoingChar + type:CBCharacteristicWriteWithResponse]; +} + +- (NSUUID *)socketServiceIdentifier:(GNSSocket *)socket { + return _cbPeripheral.identifier; +} + +- (void)disconnectSocket:(GNSSocket *)socket { + if (!socket.isConnected || _state == GNSCentralPeerManagerStateNotConnected || + _state == GNSCentralPeerManagerStateBleDisconnecting) { + return; + } + GTMLoggerInfo(@"Disconnect socket %@", socket); + [self disconnectingWithError:nil]; +} + +- (void)socketWillBeDeallocated:(GNSSocket *)socket { + if ((_state == GNSCentralPeerManagerStateBleDisconnecting) || + (_state == GNSCentralPeerManagerStateNotConnected)) { + // Already disconnecting or disconnected. Nothing to do. + return; + } + // The socket owner is dropping the socket without notice. The BLE has to be disconnected. + _socket = nil; + [self disconnectingWithError:nil]; +} + +#pragma mark - GNSWeavePacketHandler + +- (void)handleConnectionRequestPacket:(GNSWeaveConnectionRequestPacket *)packet + context:(id)context { + GTMLoggerError(@"Unexpected connection request packet received."); + [self handleWeaveError:GNSErrorUnexpectedWeaveControlPacket socket:_socket]; +} + +- (void)handleConnectionConfirmPacket:(GNSWeaveConnectionConfirmPacket *)packet + context:(id)context { + // The protocol specs guarantee that a connection confirm packet should contain a valid + // version for the client, i.e. inside the version interval sent in the connection request + // packet. If the server doesn't support any versions in the client interval an error packet + // should be sent, not a connection confirm. + NSAssert(packet.version == kWeaveVersionSupported, @"Unsupported Weave protocol version: %d.", + packet.version); + [_connectionConfirmTimer invalidate]; + _connectionConfirmTimer = nil; + _socket.packetSize = packet.packetSize; + [_socket didConnect]; + if (packet.data) { + // According to the Weave BLE protocol the data received during the connection handshake should + // be treated as the first message of the socket. + // + // Simulate a data packet being received. The packet counter value is not checked by the socket. + GNSWeaveDataPacket *dataPacket = [[GNSWeaveDataPacket alloc] initWithPacketCounter:0 + firstPacket:YES + lastPacket:YES + data:packet.data]; + [_socket didReceiveIncomingWeaveDataPacket:dataPacket]; + } +} + +- (void)handleErrorPacket:(GNSWeaveErrorPacket *)packet context:(id)context { + GTMLoggerInfo(@"Error packet received."); + [self handleWeaveError:GNSErrorWeaveErrorPacketReceived socket:_socket]; +} + +- (void)handleDataPacket:(GNSWeaveDataPacket *)packet context:(id)context { + if (packet.isFirstPacket && _socket.waitingForIncomingData) { + GTMLoggerError(@"There is already a receive operation in progress"); + [self handleWeaveError:GNSErrorWeaveDataTransferInProgress socket:_socket]; + return; + } + [_socket didReceiveIncomingWeaveDataPacket:packet]; +} + +#pragma mark - TestingHelpers + +- (NSTimer *)testing_connectionConfirmTimer { + return _connectionConfirmTimer; +} + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/GNSCentral.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/GNSCentral.h new file mode 100644 index 00000000000..01b0891b406 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/GNSCentral.h @@ -0,0 +1,17 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/GNSShared.h" diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/GNSPeripheral.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/GNSPeripheral.h new file mode 100644 index 00000000000..3be36325b0c --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/GNSPeripheral.h @@ -0,0 +1,17 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/GNSShared.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager.h" diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/GNSShared.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/GNSShared.h new file mode 100644 index 00000000000..4791bdac6c5 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/GNSShared.h @@ -0,0 +1,16 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.h" diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager+Private.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager+Private.h new file mode 100644 index 00000000000..1410b01be95 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager+Private.h @@ -0,0 +1,70 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager.h" + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h" + +@class GNSSocket; + +typedef BOOL (^GNSUpdateValueHandler)(); + +/** + * Private methods called by GNSPeripheralManager, GNSSocket and for tests. + * Should not be used by the Nearby Socket client. + */ +@interface GNSPeripheralManager ()<CBPeripheralManagerDelegate> + +@property(nonatomic, readonly) NSString *restoreIdentifier; +@property(nonatomic, readonly) CBPeripheralManager *cbPeripheralManager; + +/** + * Updates the outgoing characteristic value using an handler. The handler is stored in + * a queue. If the CBPeripheralManager is ready, the handler is called right away. Otherwise the + * handler will be called as soon as the CBPeripheralManager is ready. + * The characteristic value should be updated with + * -[GNSPeripheralManager updateValue:forCharacteristic:onSocket:]. If the update failed, + * the handler should return NO, and the handler will be rescheduled when the peripheral will ready + * again. + * + * @param socket Socket + * @param handler Handler to update the value. Should return YES if the data was sent successfully, + * and NO if the update should be scheduled for later. + */ +- (void)updateOutgoingCharOnSocket:(GNSSocket *)socket withHandler:(GNSUpdateValueHandler)handler; + +/** + * Stops BLE advertising and starts it again with the peripheral service managers who are set to be + * advertising. + */ +- (void)updateAdvertisedServices; + +/** + * Sends a packet using the outgoing characteristic for a socket. + * + * @param data Packet to send + * @param socket Socket to send the packet + * + * @return YES if the packet has been sent. + */ +- (BOOL)updateOutgoingCharacteristic:(NSData *)data onSocket:(GNSSocket *)socket; + +/** + * Informs the peripheral manager that |socket| is now disconnected. + * + * @param socket Socket. + */ +- (void)socketDidDisconnect:(GNSSocket *)socket; + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager.h new file mode 100644 index 00000000000..6a0db786fb9 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager.h @@ -0,0 +1,111 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import <CoreBluetooth/CoreBluetooth.h> +#import <Foundation/Foundation.h> + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.h" + +@class GNSPeripheralServiceManager; + +/** + * This class is in charge of the CBPeripheralManager. It owns the list of + * GNSPeripheralServiceManager to advertise. This class dispatches events received from + * the CBPeripheralManager. + * + * * To create a GNSPeripheralManager instance: + * GNSPeripheralManager *peripheralManager = + * [[GNSPeripheralManager alloc] initWithAdvertisedName:@"AdvertisedName" + * restoreIdentifier:@"BLEIdentifier"]; + * + * * To create a service: + * GNSShouldAcceptSocketHandler shouldAcceptSocketHandler = ^(GNSSocket *socket) { + * socket.delegate = mySocketDelegate; + * return YES; + * } + * GNSPeripheralServiceManager *service = + * [[GNSPeripheralServiceManager alloc] initWithBleServiceUUID:serviceUUID + * shouldAcceptSocketHandler:shouldAcceptSocketHandler]; + * [peripheralManager addPeripheralServiceManager:service bleServiceAddedCompletion:handler]; + * + * + * * When a socket is created, |shouldAcceptSocketHandler| is called. If the handler returns YES, + * the socket delegate should be set, and the socket should be retained. The socket delegate + * will be called as soon as the socket is connected. If the handler returns NO, the socket + * should not be used. + * See GNSSocket documentation to send and receive data. + * + * This class is not thread-safe. + */ +@interface GNSPeripheralManager : NSObject + +@property(nonatomic, readonly, getter=isStarted) BOOL started; +@property(nonatomic, readonly) CBManagerState peripheralManagerState; +@property(nonatomic, copy) GNSPeripheralManagerStateHandler peripheralManagerStateHandler; + +/** + * Display name used in the BLE advertisement. + * + * Note that the change of the advertisement name will only be effective on the next change of + * the advertised service UUIDs. + */ +@property(nonatomic, copy) NSString *advertisedName; + +/** + * Creates a CBPeripheralManager instance and sets itself as the delegate. Pass nil to + * |advertisedName| to avoid setting the BLE advertised name; this is useful for avoiding a name + * collision with clients that use the name in a custom discovery algorithm. + * + * @param advertisedName Display name used in the BLE advertisement + * @param restoreIdentifier Value used for CBPeripheralManagerOptionRestoreIdentifierKey + * @param queue The queue this object is called on and callbacks are made on. + * + * @return GNSPeripheralManager instance + */ +- (instancetype)initWithAdvertisedName:(NSString *)advertisedName + restoreIdentifier:(NSString *)restoreIdentifier + queue:(dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER; + +/** + * Creates a CBPeripheralManager using the main queue for callbacks. + */ +- (instancetype)initWithAdvertisedName:(NSString *)advertisedName + restoreIdentifier:(NSString *)restoreIdentifier; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Adds a new peripheral service manager into the managed services. If the GNSPeripheralManager + * is started, the new peripheral service manager will be added right away into BLE service + * database. |completion| is called everytime the service is added into the BLE service database. + * + * @param peripheralServiceManager GNSPeripheralServiceManager + * @param completion Callback called when the service was added. + */ +- (void)addPeripheralServiceManager:(GNSPeripheralServiceManager *)peripheralServiceManager + bleServiceAddedCompletion:(GNSErrorHandler)completion; + +/** + * If the bluetooth is on, all CB services will be added, and the services will be advertised + * (according to -[GNSPeripheralServiceManager advertising]. Otherwise, it will be done as soon as + * the bluetooth is turned on. + */ +- (void)start; + +/** + * Bluetooth advertisement is turned off, and all services are removed from CB. + */ +- (void)stop; + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager.m b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager.m new file mode 100644 index 00000000000..38d549c7ef1 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager.m @@ -0,0 +1,546 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager+Private.h" + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager+Private.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h" +#import "GoogleToolboxForMac/GTMLogger.h" + +#if TARGET_OS_IPHONE +#import <UIKit/UIKit.h> +#endif + +static NSString *CBManagerStateString(CBManagerState state) { + switch (state) { + case CBManagerStateUnknown: + return @"CBManagerStateUnknown"; + case CBManagerStateResetting: + return @"CBManagerStateResetting"; + case CBManagerStateUnsupported: + return @"CBManagerStateUnsupported"; + case CBManagerStateUnauthorized: + return @"CBManagerStateUnauthorized"; + case CBManagerStatePoweredOff: + return @"CBManagerStatePoweredOff"; + case CBManagerStatePoweredOn: + return @"CBManagerStatePoweredOn"; + } + return @"CBManagerState Unknown"; +} + +// http://b/28875581 On iOS, under some misterious conditions, the Bluetooth daemon (BTServer +// process) continously crashes when attempting to register Bluetooth services. In fact, if the +// BTServer process is killed, the OS spins a new instace of this process after 10 seconds. +// +// It is defined as a resetting episode a set of successive state changes of a CBPeripheralManager +// that include at least one state CBManagerStateResetting. +// As a heuristic, a resetting episode is considered a crash loop of the Bluetooth daemon iff: +// * the CBPeripheralManager enters more than 5 times in CBManagerStateResetting; +// * the time interval between successive CBManagerStateResetting notifications is +// less than 15 seconds; +// +// The minimum number of successive CBManagerStateResetting states for a resetting +// episode to be considered a Bluetooth daemon crash loop. +static NSInteger gKBTCrashLoopMinNumberOfResetting = 5; + +// The maximum time interval between successive CBManagerStateResetting states for a +// resetting episode to be considered a Bluetooth daemon crash loop. +static NSTimeInterval gKBTCrashLoopMaxTimeBetweenResetting = 15.f; + +@interface GNSPeripheralManager () { + // key: -[GNSPeripheralServiceManager serviceUUID], value: GNSPeripheralServiceManager + NSMapTable *_peripheralServiceManagers; + // Queue of blocks to update BLE values + NSMutableDictionary<NSUUID *, NSMutableArray<GNSUpdateValueHandler> *> + *_handlerQueuePerSocketIdentifier; + NSDictionary<NSString *, id> *_advertisementInProgressData; + NSDictionary<NSString *, id> *_advertisementData; + dispatch_queue_t _queue; + + // Bluetooth daemon crash info. + NSDate *_btCrashLastResettingDate; + NSInteger _btCrashCount; +} + +#if TARGET_OS_IPHONE +@property(nonatomic, assign) UIBackgroundTaskIdentifier backgroundTaskId; +#endif + +@property(nonatomic, getter=isStarted) BOOL started; + +@end + +@implementation GNSPeripheralManager + +- (instancetype)init { + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (instancetype)initWithAdvertisedName:(NSString *)advertisedName + restoreIdentifier:(NSString *)restoreIdentifier + queue:(dispatch_queue_t)queue { + self = [super init]; + if (self) { + _restoreIdentifier = [restoreIdentifier copy]; + _advertisedName = [advertisedName copy]; + _handlerQueuePerSocketIdentifier = [NSMutableDictionary dictionary]; + _peripheralServiceManagers = [NSMapTable strongToWeakObjectsMapTable]; + _btCrashLastResettingDate = [NSDate dateWithTimeIntervalSince1970:0]; + _queue = queue; + +#if TARGET_OS_IPHONE + _backgroundTaskId = UIBackgroundTaskInvalid; +#endif + } + return self; +} + +- (instancetype)initWithAdvertisedName:(NSString *)advertisedName + restoreIdentifier:(NSString *)restoreIdentifier { + return [self initWithAdvertisedName:advertisedName + restoreIdentifier:restoreIdentifier + queue:dispatch_get_main_queue()]; +} + +- (void)dealloc { + [self stop]; +} + +- (void)addPeripheralServiceManager:(GNSPeripheralServiceManager *)peripheralServiceManager + bleServiceAddedCompletion:(GNSErrorHandler)completion { + [_peripheralServiceManagers setObject:peripheralServiceManager + forKey:peripheralServiceManager.serviceUUID]; + [peripheralServiceManager addedToPeripheralManager:self bleServiceAddedCompletion:completion]; + if (_started) { + [self addBleServiceForServiceManager:peripheralServiceManager]; + } + // Update all advertised services to make sure that the right services are advertised in case + // all BLE services were already added. + [self updateAdvertisedServices]; +} + +- (void)start { + if (_started) { + GTMLoggerInfo(@"Peripheral manager already started."); + return; + } + NSMutableDictionary<NSString *, id> *options = + [@{CBCentralManagerOptionShowPowerAlertKey : @NO} mutableCopy]; +#if TARGET_OS_IPHONE + // Restored API only supported on iOS. + if (_restoreIdentifier) { + options[CBPeripheralManagerOptionRestoreIdentifierKey] = _restoreIdentifier; + } +#endif + _cbPeripheralManager = [self cbPeripheralManagerWithDelegate:self queue:_queue options:options]; + GTMLoggerInfo(@"Peripheral manager started."); + _started = YES; + // From Apple documentation |-[GNSPeripheralManager peripheralManagerDidUpdateState:]| will be + // called no matter the current bluetooth state. Also, if the CBPeripheralManager is being + // restored by iOS, |-[GNSPeripheralManager peripheralManager:willRestoreState:]| will be called + // before. Therefore, adding and advertising services have to be done in + // |peripheralManagerDidUpdateState:| method, so that the restored services will not be added + // (if there are restored services). +} + +- (void)stop { + if (!_started) { + GTMLoggerInfo(@"Peripheral manager already stopped."); + return; + } + GTMLoggerInfo(@"Peripheral manager stopped."); + _started = NO; + [self removeAllBleServicesAndStopAdvertising]; + _cbPeripheralManager.delegate = nil; + _cbPeripheralManager = nil; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@ %p: started %@, services %@>", self.class, self, + _started ? @"YES" : @"NO", + _peripheralServiceManagers.keyEnumerator]; +} + +- (void)addBleServiceForServiceManager:(GNSPeripheralServiceManager *)peripheralServiceManager { + if (peripheralServiceManager.cbServiceState != GNSBluetoothServiceStateNotAdded) { + // Ignored if the peripheralServiceManager.cbService was already added. + return; + } + [peripheralServiceManager willAddCBService]; + NSAssert(peripheralServiceManager.cbService, @"%@ had no cbService", peripheralServiceManager); + [_cbPeripheralManager addService:peripheralServiceManager.cbService]; +} + +- (void)addAllBleServicesAndStartAdvertising { + // Do not attempt to add any services if the Bluetooth Daemon is in a crash loop. + if ([self isBTCrashLoop]) { + // http://b/28875581 Adding new service when Bluetooth is powered off may be the reason for + // the Bluetooth Daemon crash loop. Avoid adding any BLE services here and wait for the next + // state change of the |_cbPeripheralManager|. + GTMLoggerError(@"Bluetooth Daemon crash loop detected crashing. Avoid adding BLE services."); + return; + } + +#if TARGET_OS_IPHONE + if (_backgroundTaskId == UIBackgroundTaskInvalid) { + GTMLoggerInfo(@"Start background task to add BLE services and start advertising."); + _backgroundTaskId = [[UIApplication sharedApplication] + beginBackgroundTaskWithName:@"Add BLE services and start advertising" + expirationHandler:^{ + GTMLoggerError(@"Application was suspended before add BLE services and start " + @"advertising finished."); + [[UIApplication sharedApplication] endBackgroundTask:_backgroundTaskId]; + _backgroundTaskId = UIBackgroundTaskInvalid; + }]; + } +#endif + + for (GNSPeripheralServiceManager *peripheralServiceManager in _peripheralServiceManagers + .objectEnumerator) { + [self addBleServiceForServiceManager:peripheralServiceManager]; + } + // Update all advertised services to make sure that the right services are advertised in case all + // BLE services were already added. + [self updateAdvertisedServices]; +} + +- (void)removeAllBleServicesAndStopAdvertising { + [_cbPeripheralManager stopAdvertising]; + _advertisementInProgressData = nil; + _advertisementData = nil; + + // Remove services and inform all service managers that their service was removed. + [_cbPeripheralManager removeAllServices]; + for (GNSPeripheralServiceManager *peripheralServiceManager in _peripheralServiceManagers + .objectEnumerator) { + [peripheralServiceManager didRemoveCBService]; + } +} + +- (void)updateAdvertisedServices { + if (!_started) { + // Do not start advertising if this peripheral manager is stopped. + return; + } + + CBManagerState cbState = _cbPeripheralManager.state; + if ((cbState != CBManagerStatePoweredOn) && (cbState != CBManagerStatePoweredOff)) { + GTMLoggerInfo(@"Do not start advertising on state %@", CBManagerStateString(cbState)); + return; + } + if (_advertisementInProgressData) { + GTMLoggerInfo(@"Another start advertising operation is in progress."); + return; + } + if (!_cbPeripheralManager.isAdvertising) { + GTMLoggerInfo(@"Reset the advertiment data as the peripheral is not advertising."); + _advertisementData = nil; + } + + BOOL allServicesAdded = YES; + NSMutableArray<CBUUID *> *serviceUUIDsToAdvertise = [NSMutableArray array]; + for (GNSPeripheralServiceManager *peripheralServiceManager in _peripheralServiceManagers + .objectEnumerator) { + if (peripheralServiceManager.cbServiceState != GNSBluetoothServiceStateAdded) { + allServicesAdded = NO; + break; + } + if (peripheralServiceManager.isAdvertising) { + [serviceUUIDsToAdvertise addObject:peripheralServiceManager.serviceUUID]; + } + } + if (!allServicesAdded) { + GTMLoggerInfo(@"Do not start advertising as not all bluetooth services are added."); + return; + } + NSMutableDictionary<NSString *, id> *advertisementData = [NSMutableDictionary dictionary]; + advertisementData[CBAdvertisementDataServiceUUIDsKey] = serviceUUIDsToAdvertise; + if (_advertisedName) { + advertisementData[CBAdvertisementDataLocalNameKey] = _advertisedName; + } + if ([advertisementData isEqual:_advertisementData]) { + GTMLoggerInfo( + @"Finished adding BLE services and starting advertising (advertisement data = %@).", + _advertisementData); +#if TARGET_OS_IPHONE + [[UIApplication sharedApplication] endBackgroundTask:_backgroundTaskId]; + _backgroundTaskId = UIBackgroundTaskInvalid; +#endif + return; + } + + GTMLoggerInfo(@"Start advertising: %@", advertisementData); + _advertisementInProgressData = [advertisementData copy]; + [_cbPeripheralManager stopAdvertising]; + [_cbPeripheralManager startAdvertising:_advertisementInProgressData]; +} + +#pragma mark - Private + +- (void)updateOutgoingCharOnSocket:(GNSSocket *)socket withHandler:(GNSUpdateValueHandler)handler { + NSUUID *socketIdentifier = socket.socketIdentifier; + NSMutableArray<GNSUpdateValueHandler> *handlerQueue = + _handlerQueuePerSocketIdentifier[socketIdentifier]; + if (!handlerQueue) { + if (handler()) return; + handlerQueue = [NSMutableArray array]; + _handlerQueuePerSocketIdentifier[socketIdentifier] = handlerQueue; + GTMLoggerInfo(@"Queueing value for socket: %@", socketIdentifier); + } + [handlerQueue addObject:[handler copy]]; +} + +- (void)processUpdateValueBlocks { + GTMLoggerInfo(@"About to send values for sockets: %@", _handlerQueuePerSocketIdentifier.allKeys); + for (NSUUID *socketIdentifier in _handlerQueuePerSocketIdentifier.allKeys) { + NSMutableArray<GNSUpdateValueHandler> *handlerQueue = + _handlerQueuePerSocketIdentifier[socketIdentifier]; + GTMLoggerInfo(@"%lu queued values to send.", (unsigned long)handlerQueue.count); + while (handlerQueue.count > 0) { + GNSUpdateValueHandler handler = handlerQueue[0]; + if (!handler()) { + GTMLoggerInfo(@"Value failed to be send, still in the queue to try later."); + break; + } + GTMLoggerInfo(@"Value sent successfully."); + [handlerQueue removeObjectAtIndex:0]; + if (handlerQueue.count == 0) { + GTMLoggerInfo(@"Removing pending value queue for socket: %@", socketIdentifier); + [_handlerQueuePerSocketIdentifier removeObjectForKey:socketIdentifier]; + } + } + } +} + +- (BOOL)updateOutgoingCharacteristic:(NSData *)data onSocket:(GNSSocket *)socket { + GNSPeripheralServiceManager *peripheralServiceManager = socket.owner; + NSAssert(peripheralServiceManager, @"%@ should have an owner.", socket); + return [_cbPeripheralManager updateValue:data + forCharacteristic:peripheralServiceManager.weaveOutgoingCharacteristic + onSubscribedCentrals:@[ socket.peerAsCentral ]]; +} + +- (CBPeripheralManager *)cbPeripheralManagerWithDelegate:(id<CBPeripheralManagerDelegate>)delegate + queue:(dispatch_queue_t)queue + options:(NSDictionary<NSString *, id> *)options { + return [[CBPeripheralManager alloc] initWithDelegate:delegate queue:queue options:options]; +} + +- (void)socketDidDisconnect:(GNSSocket *)socket { + NSAssert(!socket.connected, @"Should be disconnected"); + NSUUID *socketIdentifier = socket.socketIdentifier; + NSMutableArray<GNSUpdateValueHandler> *handlerQueue = + _handlerQueuePerSocketIdentifier[socketIdentifier]; + // The pending handlers have to be called so the associated send data completion blocks are + // called with a disconnect error. + for (GNSUpdateValueHandler handler in handlerQueue) { + handler(); + } + [_handlerQueuePerSocketIdentifier removeObjectForKey:socketIdentifier]; +} + +#pragma mark - Bluetooth Daemon Crash Loop + +- (BOOL)isBTCrashLoop { + NSTimeInterval timeSinceLastResetting = -[_btCrashLastResettingDate timeIntervalSinceNow]; + return (timeSinceLastResetting <= gKBTCrashLoopMaxTimeBetweenResetting) && + (_btCrashCount > gKBTCrashLoopMinNumberOfResetting); +} + +- (void)updateBTCrashLoopHeuristic { + NSAssert(_cbPeripheralManager.state == CBCentralManagerStateResetting, @"Unexpected CB state %@", + CBManagerStateString(_cbPeripheralManager.state)); + NSDate *now = [NSDate date]; + if ([now timeIntervalSinceDate:_btCrashLastResettingDate] > + gKBTCrashLoopMaxTimeBetweenResetting) { + _btCrashCount = 0; + } + _btCrashLastResettingDate = now; + _btCrashCount++; +} + +#pragma mark - CBPeripheralManagerDelegate + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + willRestoreState:(NSDictionary<NSString *, id> *)dict { +#if TARGET_OS_IPHONE + // Restored API only supported on iOS. + GTMLoggerInfo(@"Restore bluetooth services"); + _advertisementData = dict[CBPeripheralManagerRestoredStateAdvertisementDataKey]; + for (CBMutableService *service in dict[CBPeripheralManagerRestoredStateServicesKey]) { + GNSPeripheralServiceManager *serviceManager = + [_peripheralServiceManagers objectForKey:service.UUID]; + if (serviceManager) { + [serviceManager restoredCBService:service]; + } else { + GTMLoggerError(@"restoreFromCBService %@", service); + } + } +#endif +} + +- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheralManager { + NSAssert(peripheralManager == _cbPeripheralManager, @"Wrong peripheral manager."); + GTMLoggerInfo(@"Peripheral manager state updated: %@", + CBManagerStateString(_cbPeripheralManager.state)); + switch (_cbPeripheralManager.state) { + case CBManagerStatePoweredOn: + case CBManagerStatePoweredOff: + // As instructed by Apple engineers: + // * BLE services should be added on power or on power off. + // * BLE advertisment should be started on power on. + // + // Note 1: During testing it seems safe to start advertising when Bluetooth is turned off: + // the advertisment resumes when the state changes to power on. + // + // Note 2: It is important to keep the added services and to keep advertising on power off, + // to avoid the scenario when the application receives a power off notification right before + // being suspended by the OS. + [self addAllBleServicesAndStartAdvertising]; + break; + case CBManagerStateResetting: + // As instructed by Apple enginners, clean-up all internal state when CoreBluetooth is + // resetting. + [self updateBTCrashLoopHeuristic]; + [self removeAllBleServicesAndStopAdvertising]; + break; + case CBManagerStateUnknown: + // Clean-up all internal state when the CoreBluetooth state is unknown. + [self removeAllBleServicesAndStopAdvertising]; + break; + case CBManagerStateUnauthorized: + // Clean-up all internal state if the application is not authorized to use Bluetooth. + [self removeAllBleServicesAndStopAdvertising]; + break; + case CBManagerStateUnsupported: + // The application should have never attempted to start advertising if Bluetooth Low Energy + // is not supported on the device. + NSAssert(!peripheralManager.isAdvertising, @"Peripheral is not advertising"); + break; + } + if (_peripheralManagerStateHandler) { + _peripheralManagerStateHandler(_cbPeripheralManager.state); + } +} + +- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral + error:(NSError *)error { + _advertisementData = _advertisementInProgressData; + _advertisementInProgressData = nil; + if (error) { + GTMLoggerError(@"Start advertisment failed with error %@. Will retry to start advertising on " + "the next BLE state change notification. ", + error); + _advertisementData = nil; + return; + } + + NSAssert(peripheral.isAdvertising, @"Peripheral should be advertising."); + GTMLoggerInfo(@"Peripheral did start advertising %@", _advertisementData); + + // Once an advertisment operation is over, check if the advertised data is up-to-date. + [self updateAdvertisedServices]; +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didAddService:(CBService *)service + error:(NSError *)error { + GNSPeripheralServiceManager *peripheralServiceManager = + [_peripheralServiceManagers objectForKey:service.UUID]; + if (!peripheralServiceManager) { + GTMLoggerError(@"Unknown service %@ with %@", service, self); + return; + } + [peripheralServiceManager didAddCBServiceWithError:error]; + [self updateAdvertisedServices]; +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + central:(CBCentral *)central + didSubscribeToCharacteristic:(CBCharacteristic *)characteristic { + GNSPeripheralServiceManager *peripheralServiceManager = + [_peripheralServiceManagers objectForKey:characteristic.service.UUID]; + [peripheralServiceManager central:central didSubscribeToCharacteristic:characteristic]; +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + central:(CBCentral *)central + didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic { + GNSPeripheralServiceManager *peripheralServiceManager = + [_peripheralServiceManagers objectForKey:characteristic.service.UUID]; + [peripheralServiceManager central:central didUnsubscribeFromCharacteristic:characteristic]; + + // Restarting the peripheral manager after a disconnect. This is a workaround for b/31752176. + // Update: This problem persists in iOS 10 & 11. Discussion: https://goo.gl/fdE19G + [self stop]; + [self start]; +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didReceiveReadRequest:(CBATTRequest *)request { + GNSPeripheralServiceManager *peripheralServiceManager = + [_peripheralServiceManagers objectForKey:request.characteristic.service.UUID]; + if (!peripheralServiceManager) { + [_cbPeripheralManager respondToRequest:request withResult:CBATTErrorAttributeNotFound]; + return; + } + CBATTError requestError = [peripheralServiceManager canProcessReadRequest:request]; + if (requestError != CBATTErrorSuccess) { + [_cbPeripheralManager respondToRequest:request withResult:requestError]; + return; + } + [peripheralServiceManager processReadRequest:request]; + [_cbPeripheralManager respondToRequest:request withResult:CBATTErrorSuccess]; +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didReceiveWriteRequests:(NSArray<CBATTRequest *> *)requests { + // According to Apple's doc, if any one request cannot be processed, no request should + // be processed. Only one CBATTError should be returned with the first request. + if (requests.count == 0) { + return; + } + CBATTError requestError = CBATTErrorSuccess; + for (CBATTRequest *request in requests) { + GNSPeripheralServiceManager *peripheralServiceManager = + [_peripheralServiceManagers objectForKey:request.characteristic.service.UUID]; + if (!peripheralServiceManager) { + requestError = CBATTErrorAttributeNotFound; + break; + } + requestError = [peripheralServiceManager canProcessWriteRequest:request]; + if (requestError != CBATTErrorSuccess) { + break; + } + } + if (requestError != CBATTErrorSuccess) { + [_cbPeripheralManager respondToRequest:requests[0] withResult:requestError]; + return; + } + for (CBATTRequest *request in requests) { + GNSPeripheralServiceManager *peripheralServiceManager = + [_peripheralServiceManagers objectForKey:request.characteristic.service.UUID]; + [peripheralServiceManager processWriteRequest:request]; + } + [_cbPeripheralManager respondToRequest:requests[0] withResult:requestError]; +} + +- (void)peripheralManagerIsReadyToUpdateSubscribers:(CBPeripheralManager *)peripheral { + GTMLoggerInfo(@"Ready to update subscribers."); + [self processUpdateValueBlocks]; +} + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager+Private.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager+Private.h new file mode 100644 index 00000000000..7716786a08f --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager+Private.h @@ -0,0 +1,132 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager.h" + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h" + +typedef NS_ENUM(NSInteger, GNSBluetoothServiceState) { + GNSBluetoothServiceStateNotAdded, + GNSBluetoothServiceStateAddInProgress, + GNSBluetoothServiceStateAdded, +}; + +/** + * Private methods called by GNSPeripheralManager, GNSSocket and for tests. + * Should not be used by the Nearby Socket client. + */ +@interface GNSPeripheralServiceManager ()<GNSSocketOwner> + +@property(nonatomic, readonly) GNSPeripheralManager *peripheralManager; +@property(nonatomic, readonly) GNSBluetoothServiceState cbServiceState; +@property(nonatomic, readonly) CBMutableService *cbService; +@property(nonatomic, readonly) CBMutableCharacteristic *weaveIncomingCharacteristic; +@property(nonatomic, readonly) CBMutableCharacteristic *weaveOutgoingCharacteristic; +@property(nonatomic, readonly) CBMutableCharacteristic *pairingCharacteristic; +@property(nonatomic, readonly) GNSShouldAcceptSocketHandler shouldAcceptSocketHandler; + +/** + * Informs this service manager that its CBService will start to be added. + * + * Note that when the application is started in background on a Bluetooth event, the Bluetooth + * services are restored in state GNSBluetoothServiceStateAdded and this method will never + * called. + */ +- (void)willAddCBService; + +/** + * Informs this service manager that its CBService was added or failed to be added. + * + * Note that when the application is started in background on a Bluetooth event, the Bluetooth + * services are restored in state GNSBluetoothServiceStateAdded and this method will never + * called. + */ +- (void)didAddCBServiceWithError:(NSError *)error; + +/** + * Informs this service manager that its CBService was removed. + */ +- (void)didRemoveCBService; + +/** + * Receives the CBMutableService restored by iOS. + * + * @param service Restored service. + */ +- (void)restoredCBService:(CBMutableService *)service; + +/** + * Called only once in the instance life time. Called when the instance is attached to + * a GNSPeripheralManager instance. Should assert when this method is called twice. + * + * @param peripheralManager GNSPeripheralManager + * @param completion Callback when the BLE service is added. + */ +- (void)addedToPeripheralManager:(GNSPeripheralManager *)peripheralManager + bleServiceAddedCompletion:(GNSErrorHandler)completion; + +/** + * Checks if a read request can be processed (based on the central and the characteristic). + * + * @param request Read request + * + * @return Error if cannot process the request + */ +- (CBATTError)canProcessReadRequest:(CBATTRequest *)request; + +/** + * Process a read request. + * + * @param request Read request + */ +- (void)processReadRequest:(CBATTRequest *)request; + +/** + * Checks if a write request can be processed (based on the central and the characteristic). + * + * @param request Write request + * + * @return Error if cannot process the request + */ +- (CBATTError)canProcessWriteRequest:(CBATTRequest *)request; + +/** + * Process the data according to the signal send in the data. See GNSControlSignal. + * + * @param request Request from CBCentral + */ +- (void)processWriteRequest:(CBATTRequest *)request; + +/** + * Called when a central subscribes to a characteristic. If the characteristic is the outgoing + * characteristic, the desired connection latency is set to low. + * + * @param central The central subscribing to the characteristic + * @param characteristic The characteristic being subscribe. + */ +- (void)central:(CBCentral *)central + didSubscribeToCharacteristic:(CBCharacteristic *)characteristic; + +/** + * Called when a central unsubscribes from a characteristic. If the characteristic is the outgoing + * characteristic and a socket still exists with this central, then the socket is disconnected + * (as if received the disconnect signal). + * + * @param central The central unsubscribing to the characteristic + * @param characteristic The characteristic being unsubscribed. + */ +- (void)central:(CBCentral *)central + didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic; + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager.h new file mode 100644 index 00000000000..24f9aafca7c --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager.h @@ -0,0 +1,81 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import <CoreBluetooth/CoreBluetooth.h> +#import <Foundation/Foundation.h> + +@class GNSPeripheralManager; +@class GNSPeripheralServiceManager; +@class GNSSocket; + +typedef BOOL (^GNSShouldAcceptSocketHandler)(GNSSocket *socket); + +/** + * This class manages one BLE service. It keeps track of the list of centrals connected with + * a socket to this service. This class has also the knowledge to parse the |GNSControlSignal|. + * One instance is created per BLE service and each instance can be used only once. + * + * This class is not thread-safe. + */ +@interface GNSPeripheralServiceManager : NSObject + +/** + * Core Bluetooth service UUID. + */ +@property(nonatomic, readonly) CBUUID *serviceUUID; + +/** + * Makes this service being advertised by Core Bluetooth or not. Even if the service is not + * advertised, the service is still present in the service database, and thus can still be used + * to send/receive data. The default value is YES. + */ +@property(nonatomic, getter=isAdvertising) BOOL advertising; + +/** + * Creates an instance to manage a BLE service. The BLE service can be used to create one socket + * per central. When a central tries to connected, |shouldAcceptSocketHandler| is called with the + * socket to the central. If the handler returns YES, the socket is accepted. The socket delegate + * should be set before returning from |shouldAcceptSocketHandler| (if the socket is accepted). + * The socket will be ready to use shortly after. + * -[id<GNSSocketDelegate> socketDidConnect:] will be called once the socket + * is ready to send and receive data. The socket is retained by GNSPeripheralServiceManager as + * long as it is connected. + * + * @param serviceUUID Service UUID + * @param addPairingCharacteristic If YES, will advertise an encrypted characteristic used by + * the central if it wants to do a bluetooth pairing + * @param shouldAcceptSocketHandler Handler called when a central starts a new socket. The delegate + * socket can be set if the socket is accepted. + * @param queue The queue this object is called on and callbacks are made on. + * + * @return GNSPeripheralServiceManager instance + */ +- (instancetype)initWithBleServiceUUID:(CBUUID *)serviceUUID + addPairingCharacteristic:(BOOL)addPairingCharacteristic + shouldAcceptSocketHandler:(GNSShouldAcceptSocketHandler)shouldAcceptSocketHandler + queue:(dispatch_queue_t)queue + NS_DESIGNATED_INITIALIZER; + +/** + * Creates an instance using the main queue for callbacks. + * + * @return GNSPeripheralServiceManager instance + */ +- (instancetype)initWithBleServiceUUID:(CBUUID *)serviceUUID + addPairingCharacteristic:(BOOL)addPairingCharacteristic + shouldAcceptSocketHandler:(GNSShouldAcceptSocketHandler)shouldAcceptSocketHandler; + +- (instancetype)init NS_UNAVAILABLE; + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager.m b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager.m new file mode 100644 index 00000000000..aa4a0be22d7 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager.m @@ -0,0 +1,551 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager+Private.h" + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager+Private.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.h" +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSWeavePacket.h" +#import "GoogleToolboxForMac/GTMLogger.h" + +// The Weave BLE protocol has only one valid version. +static const UInt16 kWeaveVersionSupported = 1; + +static NSString *GetBluetoothServiceStateDescription(GNSBluetoothServiceState state) { + switch (state) { + case GNSBluetoothServiceStateNotAdded: + return @"NotAdded"; + case GNSBluetoothServiceStateAddInProgress: + return @"AddInProgress"; + case GNSBluetoothServiceStateAdded: + return @"Added"; + } + NSCAssert(NO, @"Wrong GNSBluetoothServiceState %ld", (long)state); + return @""; +} + +@interface GNSPeripheralServiceManager ()<GNSWeavePacketHandler> { + GNSPeripheralManager *_peripheralManager; + BOOL _addPairingCharacteristic; + CBMutableService *_cbService; + + // Weave characteristics UUIDs are different from legagy ones. + CBMutableCharacteristic *_weaveIncomingChar; + CBMutableCharacteristic *_weaveOutgoingChar; + + // Pairing chacteristic is used by both Weave and legacy protocols. Changing the UUID of this + // characteristic has no impact in both Weave and legacy protocols (CrOS doesn't need it). + CBMutableCharacteristic *_pairingChar; + + // key: central identifier, value: GNSPeripheral + NSMutableDictionary<NSUUID *, GNSSocket *> *_sockets; + GNSShouldAcceptSocketHandler _shouldAcceptSocketHandler; + GNSErrorHandler _bleServiceAddedCompletion; + + dispatch_queue_t _queue; +} + +@end + +static CBMutableCharacteristic *CreateWeaveIncomingCharacteristic() { + return [[CBMutableCharacteristic alloc] + initWithType:[CBUUID UUIDWithString:kGNSWeaveToPeripheralCharUUIDString] + properties:CBCharacteristicPropertyWrite + value:nil + permissions:CBAttributePermissionsWriteable]; +} + +static CBMutableCharacteristic *CreateWeaveOutgoingCharacteristic() { + return [[CBMutableCharacteristic alloc] + initWithType:[CBUUID UUIDWithString:kGNSWeaveFromPeripheralCharUUIDString] + properties:CBCharacteristicPropertyIndicate + value:nil + permissions:CBAttributePermissionsReadable]; +} + +static CBMutableCharacteristic *CreatePairingCharacteristic() { + return [[CBMutableCharacteristic alloc] + initWithType:[CBUUID UUIDWithString:kGNSPairingCharUUIDString] + properties:CBCharacteristicPropertyRead + value:nil + permissions:CBAttributePermissionsReadEncryptionRequired]; +} + +@implementation GNSPeripheralServiceManager + +- (instancetype)init { + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (instancetype)initWithBleServiceUUID:(CBUUID *)serviceUUID + addPairingCharacteristic:(BOOL)addPairingCharacteristic + shouldAcceptSocketHandler:(GNSShouldAcceptSocketHandler)shouldAcceptSocketHandler + queue:(dispatch_queue_t)queue { + self = [super init]; + if (self) { + NSParameterAssert(shouldAcceptSocketHandler); + _advertising = YES; + _serviceUUID = serviceUUID; + _addPairingCharacteristic = addPairingCharacteristic; + _shouldAcceptSocketHandler = [shouldAcceptSocketHandler copy]; + _sockets = [NSMutableDictionary dictionary]; + _queue = queue; + } + return self; +} + +- (instancetype)initWithBleServiceUUID:(CBUUID *)serviceUUID + addPairingCharacteristic:(BOOL)addPairingCharacteristic + shouldAcceptSocketHandler:(GNSShouldAcceptSocketHandler)shouldAcceptSocketHandler { + return [self initWithBleServiceUUID:serviceUUID + addPairingCharacteristic:addPairingCharacteristic + shouldAcceptSocketHandler:shouldAcceptSocketHandler + queue:dispatch_get_main_queue()]; +} + +- (NSString *)description { + return [NSString + stringWithFormat: + @"<%@: %p, %@, CBService state: %@, serviceUUID: %@, add pairing char: %@, sockets: %ld>", + [self class], self, _advertising ? @"advertising" : @"not advertising", + GetBluetoothServiceStateDescription(_cbServiceState), _serviceUUID, + _addPairingCharacteristic ? @"YES" : @"NO", (unsigned long)_sockets.count]; +} + +- (void)willAddCBService { + NSAssert(_cbServiceState == GNSBluetoothServiceStateNotAdded, @"Unexpected CBService state %@ %@", + GetBluetoothServiceStateDescription(_cbServiceState), self); + NSAssert(_cbService == nil, @"Service should not exist before it is actually added %@", self); + + _cbServiceState = GNSBluetoothServiceStateAddInProgress; + NSAssert(!_weaveIncomingChar, @"Should have no weave incoming characteristic %@.", self); + _weaveIncomingChar = CreateWeaveIncomingCharacteristic(); + NSAssert(!_weaveOutgoingChar, @"Should have no weave outgoing characteristic %@.", self); + _weaveOutgoingChar = CreateWeaveOutgoingCharacteristic(); + NSAssert(!_pairingChar, @"Should have no pairing characteristic %@.", self); + if (_addPairingCharacteristic) { + _pairingChar = CreatePairingCharacteristic(); + } + _cbService = [[CBMutableService alloc] initWithType:_serviceUUID primary:YES]; + if (_pairingChar) { + _cbService.characteristics = @[ _weaveIncomingChar, _weaveOutgoingChar, _pairingChar ]; + } else { + _cbService.characteristics = @[ _weaveIncomingChar, _weaveOutgoingChar ]; + } + GTMLoggerInfo(@"Will add BLE service: %@", _cbService); +} + +- (void)didAddCBServiceWithError:(NSError *)error { + if (_cbServiceState == GNSBluetoothServiceStateNotAdded) { + // Ignored as the service was probably removed while the add CBService operation was in + // progress. + GTMLoggerError(@"Ignore adding BLE service %@ result (error = %@) %@", _cbService, error, self); + return; + } + + if (error) { + GTMLoggerError(@"Failed adding BLE service %@ (error = %@) %@", _cbService, error, self); + [self didRemoveCBService]; + } else { + GTMLoggerInfo(@"Finished adding BLE service %@", _cbService); + _cbServiceState = GNSBluetoothServiceStateAdded; + } + if (_bleServiceAddedCompletion) { + _bleServiceAddedCompletion(error); + } +} + +- (void)didRemoveCBService { + _cbServiceState = GNSBluetoothServiceStateNotAdded; + _cbService = nil; + _weaveIncomingChar = nil; + _weaveOutgoingChar = nil; + _pairingChar = nil; +} + +- (void)restoredCBService:(CBMutableService *)service { + if (![service.UUID isEqual:_serviceUUID]) { + GTMLoggerError(@"Cannot restore from bluetooth service %@.", service); + return; + } + for (CBMutableCharacteristic *characteristic in service.characteristics) { + if (_addPairingCharacteristic && + [characteristic.UUID isEqual:[CBUUID UUIDWithString:kGNSPairingCharUUIDString]]) { + _pairingChar = characteristic; + } else if ([characteristic.UUID + isEqual:[CBUUID UUIDWithString:kGNSWeaveToPeripheralCharUUIDString]]) { + _weaveIncomingChar = characteristic; + } else if ([characteristic.UUID + isEqual:[CBUUID UUIDWithString:kGNSWeaveFromPeripheralCharUUIDString]]) { + _weaveOutgoingChar = characteristic; + } + } + // TODO(jlebel): Need to find out how the service can be restored if a characteritic is missing. + // b/25561363 + NSAssert(_weaveIncomingChar, @"Weave incoming characteristic missing"); + NSAssert(_weaveOutgoingChar, @"Weave outgoing characteristic missing"); + NSAssert(_pairingChar || !_addPairingCharacteristic, @"Pairing characteristic missing"); + _cbService = service; + _cbServiceState = GNSBluetoothServiceStateAdded; + GTMLoggerInfo(@"Service restored: %@", _cbService); +} + +- (void)setAdvertising:(BOOL)advertising { + if (advertising == _advertising) { + return; + } + _advertising = advertising; + [_peripheralManager updateAdvertisedServices]; +} + +#pragma mark - Private + +- (GNSShouldAcceptSocketHandler)shouldAcceptSocketHandler { + return _shouldAcceptSocketHandler; +} + +- (GNSPeripheralManager *)peripheralManager { + return _peripheralManager; +} + +- (CBMutableService *)cbService { + return _cbService; +} + +- (CBMutableCharacteristic *)weaveIncomingCharacteristic { + return _weaveIncomingChar; +} + +- (CBMutableCharacteristic *)weaveOutgoingCharacteristic { + return _weaveOutgoingChar; +} + +- (CBMutableCharacteristic *)pairingCharacteristic { + return _pairingChar; +} + +- (GNSErrorHandler)bleServiceAddedCompletion { + return _bleServiceAddedCompletion; +} + +- (void)addedToPeripheralManager:(GNSPeripheralManager *)peripheralManager + bleServiceAddedCompletion:(GNSErrorHandler)completion { + NSAssert(_peripheralManager == nil, + @"Do not reuse. Already associated with another peripheral manager %@", + _peripheralManager); + _peripheralManager = peripheralManager; + _bleServiceAddedCompletion = [completion copy]; +} + +- (CBATTError)canProcessReadRequest:(CBATTRequest *)request { + if ([request.characteristic.UUID isEqual:_weaveOutgoingChar.UUID] || + [request.characteristic.UUID isEqual:_weaveIncomingChar.UUID]) { + return CBATTErrorReadNotPermitted; + } + if (_pairingChar && [request.characteristic.UUID isEqual:_pairingChar.UUID]) { + return CBATTErrorSuccess; + } + return CBATTErrorAttributeNotFound; +} + +- (void)processReadRequest:(CBATTRequest *)request { + if (!_pairingChar || ![request.characteristic.UUID isEqual:_pairingChar.UUID]) { + NSAssert(NO, @"Cannot read characteristic %@", request); + return; + } + request.value = [NSData data]; +} + +- (CBATTError)canProcessWriteRequest:(CBATTRequest *)request { + if ([request.characteristic.UUID isEqual:_weaveIncomingChar.UUID]) { + return CBATTErrorSuccess; + } else if ([request.characteristic.UUID isEqual:_weaveOutgoingChar.UUID]) { + return CBATTErrorWriteNotPermitted; + } + return CBATTErrorAttributeNotFound; +} + +- (void)handleWeaveError:(GNSError)errorCode socket:(GNSSocket *)socket { + if (!socket) { + return; + } + // Only send the error if no error packet was previously received. + if (errorCode != GNSErrorWeaveErrorPacketReceived) { + GTMLoggerInfo(@"Sending error packet for socket: %@", socket); + GNSWeaveErrorPacket *errorPacket = + [[GNSWeaveErrorPacket alloc] initWithPacketCounter:socket.sendPacketCounter]; + [self sendPacket:errorPacket toSocket:socket completion:^{ + GTMLoggerInfo(@"Error packet sent to socket: %@", socket); + }]; + } + NSError *error = GNSErrorWithCode(errorCode); + [self removeSocket:socket withError:error]; +} + +- (void)processWeaveWriteRequest:(CBATTRequest *)request { + if (![request.characteristic.UUID isEqual:_weaveIncomingChar.UUID]) { + GTMLoggerError(@"Cannot process %@ on characteristic %@ (%@)", request, + request.characteristic.UUID, + GNSCharacteristicName(request.characteristic.UUID.UUIDString)); + return; + } + GNSSocket *socket = _sockets[request.central.identifier]; + // We truncate the packet to size we are expecting: kGNSMinSupportedPacketSize (for the first + // packet) and |socket.packetSize| (for the rest). + NSUInteger truncatedSize = + MIN(socket ? socket.packetSize : kGNSMinSupportedPacketSize, request.value.length); + if (truncatedSize != request.value.length) { + GTMLoggerInfo(@"Packet with %ld bytes truncated to %ld.", (long)request.value.length, + (long)truncatedSize); + } + NSData *packetData = [request.value subdataWithRange:NSMakeRange(0, truncatedSize)]; + NSError *parsingError = nil; + GNSWeavePacket *packet = [GNSWeavePacket parseData:packetData error:&parsingError]; + if (!packet) { + GTMLoggerError(@"Error parsing weave packet (error = %@).", parsingError); + [self handleWeaveError:GNSErrorParsingWeavePacket socket:socket]; + return; + } + if (!socket && ![packet isKindOfClass:[GNSWeaveConnectionRequestPacket class]]) { + GTMLoggerInfo(@"Non-request weave packet received when no socket exists -- ignoring"); + return; + } + UInt8 expectedCounter = [packet isKindOfClass:[GNSWeaveConnectionRequestPacket class]] ? + 0 : socket.receivePacketCounter; + if (packet.packetCounter != expectedCounter) { + GTMLoggerError(@"Wrong packet counter, [received %d, expected %d].", packet.packetCounter, + expectedCounter); + [self handleWeaveError:GNSErrorWrongWeavePacketCounter socket:socket]; + return; + } + [packet visitWithHandler:self context:request]; + // A new socket was created if |request| contains a connection request packet. So we need to + // update |socket| before incrementing the packet counter. + socket = _sockets[request.central.identifier]; + + // Only an error packet can cause the socket to have been removed at this point. + NSAssert(socket || [packet isKindOfClass:[GNSWeaveErrorPacket class]], + @"Socket missing after receiving non-error weave packet"); + [socket incrementReceivePacketCounter]; +} + +- (void)processWriteRequest:(CBATTRequest *)request { + if (![request.characteristic.UUID isEqual:_weaveIncomingChar.UUID]) { + GTMLoggerError(@"Cannot process %@ on characteristic %@ (%@)", request, + request.characteristic.UUID, + GNSCharacteristicName(request.characteristic.UUID.UUIDString)); + return; + } + GTMLoggerInfo(@"Write request on Weave characteristic."); + [self processWeaveWriteRequest:request]; +} + +// This method sends |packet| fitting a single characteristic write to |socket|. All packets send by +// this class (not the socket) must use this method. +- (void)sendPacket:(GNSWeavePacket *)packet + toSocket:(GNSSocket *)socket + completion:(void (^)(void))completion { + NSAssert(packet.packetCounter == socket.sendPacketCounter, @"Wrong packet counter."); + [socket incrementSendPacketCounter]; + // Do not check if the socket before retrying to send the packet if it's a connection confirm + // packet, as the socket is only connected after that packet is sent. + BOOL checkConnected = ![packet isKindOfClass:[GNSWeaveConnectionConfirmPacket class]]; + NSData *data = [packet serialize]; + [self sendData:data toSocket:socket checkConnected:checkConnected completion:completion]; +} + +- (void)sendData:(NSData *)data + toSocket:(GNSSocket *)socket + checkConnected:(BOOL)checkConnected + completion:(void (^)(void))completion { + __weak __typeof__(self) weakSelf = self; + [_peripheralManager + updateOutgoingCharOnSocket:socket + withHandler:^() { + __typeof__(weakSelf) strongSelf = weakSelf; + if (!strongSelf || (checkConnected && !socket.isConnected)) { + // Socket is gone or disconnected; don't reschedule. + return YES; + } + if (![strongSelf.peripheralManager updateOutgoingCharacteristic:data + onSocket:socket]) { + GTMLoggerInfo(@"Failed to update characteristic value; reschedule"); + return NO; + } + if (completion) { + dispatch_async(_queue, ^{ completion(); }); + } + return YES; + }]; +} + +- (void)socketReady:(GNSSocket *)socket { + [socket didConnect]; +} + +- (void)removeSocket:(GNSSocket *)socket withError:(NSError *)error { + if (error) { + GTMLoggerInfo(@"Socket disconnected with error, socket: %@, error: %@", socket, error); + } else { + GTMLoggerInfo(@"Socket disconnected %@", socket); + } + [_sockets removeObjectForKey:socket.peerIdentifier]; + [socket didDisconnectWithError:error]; + [_peripheralManager socketDidDisconnect:socket]; +} + +- (void)central:(CBCentral *)central + didSubscribeToCharacteristic:(CBCharacteristic *)characteristic { + if ([characteristic.UUID isEqual:_weaveOutgoingChar.UUID]) { + // TODO(sacomoto): The latency should be changed only with CrOS. When iOS is talking with OS X, + // changing the latency increases the authentication time. See b/23719877. + [_peripheralManager.cbPeripheralManager + setDesiredConnectionLatency:CBPeripheralManagerConnectionLatencyLow + forCentral:central]; + } +} + +- (void)central:(CBCentral *)central + didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic { + GNSSocket *socket = _sockets[central.identifier]; + if (socket && [characteristic.UUID isEqual:_weaveOutgoingChar.UUID]) { + // Disconnect signal is optional. If the central unsubscribe to _outgoingChar, the socket + // is disconnected. + GTMLoggerInfo(@"%@ unsubscribe to outgoing characteristic", central); + [self removeSocket:socket withError:nil]; + } +} + +#pragma mark - GNSSocketOwner + +- (NSUInteger)socketMaximumUpdateValueLength:(GNSSocket *)socket { + return [socket.peerAsCentral maximumUpdateValueLength]; +} + +- (void)sendData:(NSData *)data socket:(GNSSocket *)socket completion:(GNSErrorHandler)completion { + GTMLoggerInfo(@"Updating value in characteristic"); + [_peripheralManager updateOutgoingCharOnSocket:socket withHandler:^{ + BOOL wasSent = [_peripheralManager updateOutgoingCharacteristic:data onSocket:socket]; + if (wasSent) { + GTMLoggerInfo(@"Successfully updated value in characteristic"); + dispatch_async(_queue, ^{ completion(nil); }); + } else { + GTMLoggerInfo(@"Failed to update characteristic value; reschedule"); + } + return wasSent; + }]; +} + +- (NSUUID *)socketServiceIdentifier:(GNSSocket *)socket { + return [[NSUUID alloc] initWithUUIDString:_serviceUUID.UUIDString]; +} + +- (void)disconnectSocket:(GNSSocket *)socket { + if (!socket.isConnected) { + return; + } + // The Weave protocol has no disconnect signal. As a temporary solution, we send an error + // packet, as this will cause the central to disconnect. + GNSWeaveErrorPacket *errorPacket = + [[GNSWeaveErrorPacket alloc] initWithPacketCounter:socket.sendPacketCounter]; + __weak __typeof__(self) weakSelf = self; + [self sendPacket:errorPacket + toSocket:socket + completion:^{ + if (weakSelf) { + GTMLoggerInfo(@"Error packet sent (to force the central to disconnect)."); + [weakSelf removeSocket:socket withError:nil]; + } + }]; +} + +- (void)socketWillBeDeallocated:(GNSSocket *)socket { +} + +#pragma mark - GNSWeavePacketHandler + +- (void)handleConnectionRequestPacket:(GNSWeaveConnectionRequestPacket *)packet + context:(id)request { + NSAssert([request isKindOfClass:[CBATTRequest class]], @"The context should be a request."); + GNSSocket *socket = _sockets[((CBATTRequest *)request).central.identifier]; + if (socket) { + GTMLoggerInfo(@"Receiving a connection request from an already connected socket %@.", socket); + // The peripheral considers the previous socket as being disconnected. + NSError *error = GNSErrorWithCode(GNSErrorNewInviteToConnectReceived); + [self removeSocket:socket withError:error]; + socket = nil; + } + socket = [[GNSSocket alloc] initWithOwner:self + centralPeer:((CBATTRequest *)request).central + queue:_queue]; + if (packet.maxVersion < kWeaveVersionSupported || packet.minVersion > kWeaveVersionSupported) { + GTMLoggerError(@"Unsupported Weave version range: [%d, %d].", packet.minVersion, + packet.maxVersion); + [self handleWeaveError:GNSErrorUnsupportedWeaveProtocolVersion socket:socket]; + return; + } + if (packet.maxPacketSize == 0) { + socket.packetSize = [self socketMaximumUpdateValueLength:socket]; + } else { + socket.packetSize = MIN(packet.maxPacketSize, [self socketMaximumUpdateValueLength:socket]); + } + if (_shouldAcceptSocketHandler(socket)) { + _sockets[socket.peerIdentifier] = socket; + + dispatch_async(_queue, ^{ + GTMLoggerInfo(@"Sending connection confirm packet."); + GNSWeaveConnectionConfirmPacket *confirm = + [[GNSWeaveConnectionConfirmPacket alloc] initWithVersion:kWeaveVersionSupported + packetSize:socket.packetSize + data:nil]; + __weak __typeof__(self) weakSelf = self; + [self sendPacket:confirm toSocket:socket completion:^{ + if (weakSelf) { + GTMLoggerInfo(@"Connection confirm packet sent."); + [weakSelf socketReady:socket]; + } + }]; + }); + } +} + +- (void)handleConnectionConfirmPacket:(GNSWeaveConnectionConfirmPacket *)packet + context:(id)request { + NSAssert([request isKindOfClass:[CBATTRequest class]], @"The context should be a request."); + GNSSocket *socket = _sockets[((CBATTRequest *)request).central.identifier]; + GTMLoggerError(@"Unexpected connection confirm packet received."); + [self handleWeaveError:GNSErrorUnexpectedWeaveControlPacket socket:socket]; +} + +- (void)handleErrorPacket:(GNSWeaveErrorPacket *)packet context:(id)request { + NSAssert([request isKindOfClass:[CBATTRequest class]], @"The context should be a request."); + GNSSocket *socket = _sockets[((CBATTRequest *)request).central.identifier]; + GTMLoggerInfo(@"Error packet received."); + [self handleWeaveError:GNSErrorWeaveErrorPacketReceived socket:socket]; +} + +- (void)handleDataPacket:(GNSWeaveDataPacket *)packet context:(id)request { + NSAssert([request isKindOfClass:[CBATTRequest class]], @"The context should be a request."); + GNSSocket *socket = _sockets[((CBATTRequest *)request).central.identifier]; + if (packet.isFirstPacket && socket.waitingForIncomingData) { + GTMLoggerError(@"There is already a receive operation in progress"); + [self handleWeaveError:GNSErrorWeaveDataTransferInProgress socket:socket]; + return; + } + [socket didReceiveIncomingWeaveDataPacket:packet]; +} + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h new file mode 100644 index 00000000000..434dbf9e0e5 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h @@ -0,0 +1,144 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.h" + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils+Private.h" + +@class GNSWeaveDataPacket; + +/** + * Protocol implemented by the class who is in charge to create the socket. The owner knows how + * send and receive data through Bluetooth BLE. + */ +@protocol GNSSocketOwner<NSObject> + +/** + * Returns the current MTU. Called by the socket when it needs to create chunks to send data. + * + * @param socket Current socket + * + * @return MTU. + */ +- (NSUInteger)socketMaximumUpdateValueLength:(GNSSocket *)socket; + +/** + * Called by the socket when it needs to send a chunk of data. + * + * @param data Chunk to send + * @param completion Callback called when the data has been fully sent or an error has occurred. + */ +- (void)sendData:(NSData *)data socket:(GNSSocket *)socket completion:(GNSErrorHandler)completion; + +/** + * Returns the UUID of the peer. + * + * @param socket Current socket + * + * @return UUID based on the peer. + */ +- (NSUUID *)socketServiceIdentifier:(GNSSocket *)socket; + +/** + * Called by the socket when the socket should be disconnected. + * + * @param socket Current socket + */ +- (void)disconnectSocket:(GNSSocket *)socket; + +/** + * Called when the socket is deallocated. Some owners (like GNSCentralPeerManager) doesn't retain + * the socket and let the socket user taking care of the retain/release. When this method is called + * the socket should not be called anymore. + * + * @param socket Socket being deallocated. + */ +- (void)socketWillBeDeallocated:(GNSSocket *)socket; + +@end + +/** + * Private methods called by GNSSocket and for tests. + * Should not be used by the Nearby Socket client. + */ +@interface GNSSocket () + +@property(nonatomic, readwrite) UInt16 packetSize; +@property(nonatomic, readonly) UInt8 receivePacketCounter; +@property(nonatomic, readonly) UInt8 sendPacketCounter; +@property(nonatomic, readonly) id<GNSSocketOwner> owner; +@property(nonatomic, readonly) dispatch_queue_t queue; + +- (instancetype)initWithOwner:(id<GNSSocketOwner>)owner + centralPeer:(CBCentral *)centralPeer + queue:(dispatch_queue_t)queue; +- (instancetype)initWithOwner:(id<GNSSocketOwner>)owner + peripheralPeer:(CBPeripheral *)peripheralPeer + queue:(dispatch_queue_t)queue; + +/** + * Increments |receivePacketCounter|. + */ +- (void)incrementReceivePacketCounter; + +/** + * Increments |sendPacketCounter|. + */ +- (void)incrementSendPacketCounter; + +/** + * Called by the socket owner once the connection response packet has been sent or received. + * The socket is ready to be used when this method is called. + */ +- (void)didConnect; + +/** + * Called by the socket owner when the socket is disconnected and should not be used anymore. + * The socket delegate is called to be notified with: + * -[id<GNSSocketDelegate> socket:didDisconnectWithError:] + * + * @param error Error that caused the socket to disconnect. Nil if the socket was disconnected + * properly. + */ +- (void)didDisconnectWithError:(NSError *)error; + +/** + * Called by the socket owner when a data packet is received. + * + * @param dataPacket The data packet received. + */ +- (void)didReceiveIncomingWeaveDataPacket:(GNSWeaveDataPacket *)dataPacket; + +/** + * Returns YES if an incoming message is pending and more chunks are waited. + * + * @return BOOL + */ +- (BOOL)waitingForIncomingData; + +/** + * Returns the peer casted as CBPeripheral. Asserts if the socket was created with another type. + * + * @return Peer. + */ +- (CBPeripheral *)peerAsPeripheral; + +/** + * Returns the peer casted as CBCentral. Asserts if the socket was created with another type. + * + * @return Peer. + */ +- (CBCentral *)peerAsCentral; + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.h new file mode 100644 index 00000000000..3c4daba09fb --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.h @@ -0,0 +1,155 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.h" + +@class CBCentral; +@class GNSPeripheralServiceManager; +@class GNSSocket; + +// |progress| contains the percentage of the message already sent. +typedef void (^GNSProgressHandler)(float progress); + +/** + * Protocol for socket delegates. The socket delegate should be set when the socket is accepted + * (when used with GNSPeripheralServiceManager) or when the socket is created (when used with + * GNSCentralPeer). + */ +@protocol GNSSocketDelegate<NSObject> + +/** + * Called when the socket is ready to send or receive data. + * + * @param socket Socket + */ +- (void)socketDidConnect:(GNSSocket *)socket; + +/** + * Called when the socket has been disconnected by the central (because a disconnect signal was + * received or -[GNSSocket disconnect] has been called). + * + * @param socket Socket + * @param error Error that caused the socket to disconnect. Nil if the socket was disconnected + * properly (by calling -[GNSSocket disconnect] on this side or the other side of the socket). + */ +- (void)socket:(GNSSocket *)socket didDisconnectWithError:(NSError *)error; + +/** + * Called when a new message has been received. + * + * @param socket Socket + * @param data Message received + */ +- (void)socket:(GNSSocket *)socket didReceiveData:(NSData *)data; + +@end + +/** + * This class is in charge of receiving and sending data between one central and one peripheral. It + * is created and owned by GNSPeripheralServiceManager or GNSCentralPeer. + * + * + * * To create a socket, see the GNSPeripheralManager or GNSCentralPeer documentation. + * When the socket is created, set the socket delegate. As soon as it is ready to use, the delegate + * is called: + * - (void)socketDidConnect:(GNSSocket *)socket { + * // the socket is ready to be used. To send or receive data. + * } + * + * * To receive data: + * - (void)socket:(GNSSocket *)socket didReceiveData:(NSData *)data { + * NSLog(@"Data received from central %@", data); + * ... + * } + * + * * To send data: + * GNSErrorHandler completionHandler = ^(NSError *error) { + * if (error) { + * NSLog(@"Failed to send data") + * } else { + * NSLog(@"data has been sent"); + * } + * } + * [socket sendData:dataToSend + * completion:completionHandler]; + * + * * To disconnect: + * [mySocket disconnect]; + * + * * Once the socket is disconnected (by -[GNSSocket disconnect] or by the peer): + * - (void)socket:(GNSSocket *)socket didDisconnectWithError:(NSError *)error { + * NSLog(@"Socket disconnected, by peer"); + * ... + * } +*/ +@interface GNSSocket : NSObject + +/** + * The socket delegate. + */ +@property(nonatomic, weak) id<GNSSocketDelegate> delegate; + +/** + * YES if the socket is connected. The physical BLE connection may still exist when this is NO. + */ +@property(nonatomic, readonly, getter=isConnected) BOOL connected; + +/** + * The Core Bluetooth peer identifier. + */ +@property(nonatomic, readonly) NSUUID *peerIdentifier; + +/** + * The Core Bluetooth service identifier. + */ +@property(nonatomic, readonly) NSUUID *serviceIdentifier; + +/** + * The socket identifier. + */ +@property(nonatomic, readonly) NSUUID *socketIdentifier; + +/** + * A peer socket is always created by the library. + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + * Returns YES if there is already a send operation in progress. If this method returns NO, + * then a call to -[GNSSocket sendData:completion:] won't result in a |GNSErrorOperationInProgress| + * error. + */ +- (BOOL)isSendOperationInProgress; + +/** + * Sends data to the central. The completion has to be called before calling this method a second + * time. + * + * @param data Data to send + * @param progressHandler Called repeatedly for progress feedback while the data is being sent. + * @param completion Callback called when the data has been fully sent, + * or it has failed to be sent (socket not connected or disconnected). + */ +- (void)sendData:(NSData *)data + progressHandler:(GNSProgressHandler)progressHandler + completion:(GNSErrorHandler)completion; + +/** + * Sends the disconnect signal and then disconnects this connection. The socket cannot be + * disconnected if the socket is processing data to send. The current send data operation has + * to be cancelled first. + */ +- (void)disconnect; + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.m b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.m new file mode 100644 index 00000000000..2a1c342f53c --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.m @@ -0,0 +1,238 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h" + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSWeavePacket.h" +#import "GoogleToolboxForMac/GTMLogger.h" + +typedef void (^GNSIncomingChunkReceivedBlock)(NSData *incomingData); + +@interface GNSSocket () { + NSMutableData *_incomingBuffer; + id _peer; +} + +// Handler to generate a chunk for sending data. When the bluetooth stack is ready to send +// more data the handler is called recursively to send the next chunk. +@property(nonatomic, copy) GNSSendChunkBlock sendChunkCallback; + +// Handler to receive chunk from the central. +@property(nonatomic, copy) GNSIncomingChunkReceivedBlock incomingChunkReceivedCallback; + +@property(nonatomic, readwrite, assign, getter=isConnected) BOOL connected; + +@property(nonatomic, readwrite) NSUUID *socketIdentifier; + +- (instancetype)initWithOwner:(id<GNSSocketOwner>)owner + peer:(id)peer + queue:(dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER; + +@end + +@implementation GNSSocket + +- (void)dealloc { + [_owner socketWillBeDeallocated:self]; +} + +- (BOOL)isSendOperationInProgress { + return self.sendChunkCallback != nil; +} + +- (void)sendData:(NSData *)data + progressHandler:(GNSProgressHandler)progressHandler + completion:(GNSErrorHandler)completion { + void (^callCompletion)(NSError *) = ^(NSError *error) { + if (completion) dispatch_async(_queue, ^{ completion(error); }); + }; + + if (self.sendChunkCallback) { + GTMLoggerInfo(@"Send operation already in progress"); + callCompletion(GNSErrorWithCode(GNSErrorOperationInProgress)); + return; + } + data = [data copy]; + NSUInteger totalDataSize = data.length; + GTMLoggerInfo(@"Sending data with size %lu", (unsigned long)totalDataSize); + + // Capture self for the duration of the send operation, to ensure it is completely sent. + // If the connection is lost, the block will be deleted and the retain cycle broken. + __typeof__(self) selfRef = self; // this avoids the retain cycle compiler warning + self.sendChunkCallback = ^(NSUInteger offset) { + __typeof__(self) self = selfRef; + if (!self.isConnected) { + self.sendChunkCallback = nil; + callCompletion(GNSErrorWithCode(GNSErrorNoConnection)); + return; + } + if (progressHandler) { + float progressPercentage = 1.0; + if (totalDataSize > 0) { + progressPercentage = (float)offset / totalDataSize; + } + progressHandler(progressPercentage); + } + NSUInteger newOffset = offset; + GNSWeaveDataPacket *dataPacket = + [GNSWeaveDataPacket dataPacketWithPacketCounter:self.sendPacketCounter + packetSize:self.packetSize + data:data + offset:&newOffset]; + [self incrementSendPacketCounter]; + + GTMLoggerInfo(@"Sending chunk with size %ld", (long)(newOffset - offset)); + [self.owner sendData:[dataPacket serialize] socket:self completion:^(NSError *_Nullable error) { + if (error) { + GTMLoggerInfo(@"Error sending chunk"); + self.sendChunkCallback = nil; + callCompletion(error); + } else { + if (newOffset < totalDataSize) { + // Dispatch async to avoid stack overflow on large payloads. + dispatch_async(self.queue, ^{ self.sendChunkCallback(newOffset); }); + } else { + GTMLoggerInfo(@"Finished sending payload"); + self.sendChunkCallback = nil; + callCompletion(nil); + } + } + }]; + }; + self.sendChunkCallback(0); +} + +- (void)disconnect { + if (!_connected) { + GTMLoggerInfo(@"Socket already disconnected, socket: %@, delegate %@, owner %@", self, + _delegate, _owner); + return; + } + GTMLoggerInfo(@"Disconnect"); + [_owner disconnectSocket:self]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p, %@, central: %@>", [self class], self, + _connected ? @"connected" : @"not connected", + self.peerIdentifier.UUIDString]; +} + +- (NSUUID *)peerIdentifier { + return (NSUUID *)[_peer identifier]; +} + +- (NSUUID *)serviceIdentifier { + return [_owner socketServiceIdentifier:self]; +} + +#pragma mark - Private + +- (instancetype)init { + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (instancetype)initWithOwner:(id<GNSSocketOwner>)owner + centralPeer:(CBCentral *)centralPeer + queue:(dispatch_queue_t)queue { + return [self initWithOwner:owner peer:centralPeer queue:queue]; +} + +- (instancetype)initWithOwner:(id<GNSSocketOwner>)owner + peripheralPeer:(CBPeripheral *)peripheralPeer + queue:(dispatch_queue_t)queue { + return [self initWithOwner:owner peer:peripheralPeer queue:queue]; +} + +- (instancetype)initWithOwner:(id<GNSSocketOwner>)owner + peer:(id)peer + queue:(dispatch_queue_t)queue { + self = [super init]; + if (self) { + NSAssert(owner, @"Socket should have an owner."); + NSAssert(peer, @"Socket should have a peer."); + _owner = owner; + _peer = peer; + _queue = queue; + _socketIdentifier = [NSUUID UUID]; + _packetSize = kGNSMinSupportedPacketSize; + } + return self; +} + +- (void)incrementReceivePacketCounter { + _receivePacketCounter = (_receivePacketCounter + 1) % kGNSMaxPacketCounterValue; + GTMLoggerDebug(@"New receive packet counter %d", _receivePacketCounter); +} + +- (void)incrementSendPacketCounter { + _sendPacketCounter = (_sendPacketCounter + 1) % kGNSMaxPacketCounterValue; + GTMLoggerDebug(@"New send packet counter %d", _sendPacketCounter); +} + +- (void)didConnect { + _connected = YES; + [_delegate socketDidConnect:self]; +} + +- (void)didDisconnectWithError:(NSError *)error { + _connected = NO; + _incomingChunkReceivedCallback = nil; + _incomingBuffer = nil; + [_delegate socket:self didDisconnectWithError:error]; +} + +- (void)didReceiveIncomingWeaveDataPacket:(GNSWeaveDataPacket *)dataPacket { + if (!_connected) { + GTMLoggerError(@"Cannot receive incoming data packet while not being connected"); + return; + } + if (dataPacket.isFirstPacket) { + NSAssert(!_incomingBuffer, @"There should not be a receive operation in progress."); + _incomingBuffer = [NSMutableData data]; + } + GTMLoggerInfo(@"Received chunk with size %lu", (unsigned long)dataPacket.data.length); + [_incomingBuffer appendData:dataPacket.data]; + if (dataPacket.isLastPacket) { + NSData *incomingData = _incomingBuffer; + _incomingBuffer = nil; + GTMLoggerInfo(@"Finished receiving payload with size %lu", (unsigned long)incomingData.length); + [self.delegate socket:self didReceiveData:incomingData]; + } +} + +- (BOOL)waitingForIncomingData { + return _incomingBuffer != nil; +} + +- (CBPeripheral *)peerAsPeripheral { + NSAssert([_peer isKindOfClass:[CBPeripheral class]], @"Wrong peer type %@", _peer); + if ([_peer isKindOfClass:[CBPeripheral class]]) { + return _peer; + } else { + return nil; + } +} + +- (CBCentral *)peerAsCentral { + NSAssert([_peer isKindOfClass:[CBCentral class]], @"Wrong peer type %@", _peer); + if ([_peer isKindOfClass:[CBCentral class]]) { + return _peer; + } else { + return nil; + } +} + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils+Private.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils+Private.h new file mode 100644 index 00000000000..cea06b143a7 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils+Private.h @@ -0,0 +1,47 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.h" + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const kGNSWeaveToPeripheralCharUUIDString; +extern NSString *const kGNSWeaveFromPeripheralCharUUIDString; + +extern NSString *const kGNSPairingCharUUIDString; + +@protocol GNSPeer; + +typedef void (^GNSBoolHandler)(BOOL flag); +typedef void (^GNSSendChunkBlock)(NSUInteger offset); + +/** + * Returns NSError with the description. + * + * @param errorCode Error code from GNSError. + * + * @return NSError. + */ +NSError *GNSErrorWithCode(GNSError errorCode); + +/** + * Returns a human readable name based on a characteristic UUID. + * + * @param uuid Characteristic UUID string + * + * @return Name of the characteristic. + */ +NSString *GNSCharacteristicName(NSString *uuid); + +NS_ASSUME_NONNULL_END diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.h new file mode 100644 index 00000000000..80b57882dc7 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.h @@ -0,0 +1,47 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import <CoreBluetooth/CoreBluetooth.h> + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const kGNSSocketsErrorDomain; + +typedef NS_ENUM(NSInteger, GNSError) { + GNSErrorNoError, + GNSErrorNoConnection, + GNSErrorLostConnection, + GNSErrorOperationInProgress, + GNSErrorMissingService, + GNSErrorMissingCharacteristics, + GNSErrorPeripheralDidRefuseConnection, + GNSErrorCancelPendingSocketRequested, + GNSErrorNewInviteToConnectReceived, + GNSErrorWeaveErrorPacketReceived, + GNSErrorUnsupportedWeaveProtocolVersion, + GNSErrorUnexpectedWeaveControlPacket, + GNSErrorParsingWeavePacket, + GNSErrorWrongWeavePacketCounter, + GNSErrorWeaveDataTransferInProgress, + GNSErrorParsingWeavePacketTooSmall, + GNSErrorParsingWeavePacketTooLarge, + GNSErrorConnectionTimedOut, +}; + +// This handler is called to receive the CBPeripheralManager state updates. +typedef void (^GNSPeripheralManagerStateHandler)(CBManagerState peripheralManagerState); + +typedef void (^GNSErrorHandler)(NSError *_Nullable error); + +NS_ASSUME_NONNULL_END diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.m b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.m new file mode 100644 index 00000000000..521e16045ae --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.m @@ -0,0 +1,102 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils+Private.h" + +#import <CommonCrypto/CommonDigest.h> + +NS_ASSUME_NONNULL_BEGIN + +NSString *const kGNSSocketsErrorDomain = @"com.google.nearby.sockets"; + +NSString *const kGNSWeaveToPeripheralCharUUIDString = @"00000100-0004-1000-8000-001A11000101"; +NSString *const kGNSWeaveFromPeripheralCharUUIDString = @"00000100-0004-1000-8000-001A11000102"; + +NSString *const kGNSPairingCharUUIDString = @"17836FBD-8C6A-4B81-83CE-8560629E834B"; + +NSError *GNSErrorWithCode(GNSError errorCode) { + NSString *description = nil; + switch (errorCode) { + case GNSErrorNoError: + NSCAssert(NO, @"Should not create an error with GNSErrorNoError"); + break; + case GNSErrorNoConnection: + description = @"No connection."; + break; + case GNSErrorLostConnection: + description = @"Connection lost."; + break; + case GNSErrorOperationInProgress: + description = @"Operation in progress."; + break; + case GNSErrorMissingService: + description = @"Missing service."; + break; + case GNSErrorMissingCharacteristics: + description = @"Missing characteristic."; + break; + case GNSErrorPeripheralDidRefuseConnection: + description = @"Peripheral refused connection."; + break; + case GNSErrorCancelPendingSocketRequested: + description = @"Pending socket request canceled."; + break; + case GNSErrorNewInviteToConnectReceived: + description = @"Second invitation to connect received"; + break; + case GNSErrorWeaveErrorPacketReceived: + description = @"Weave error packet received."; + break; + case GNSErrorUnsupportedWeaveProtocolVersion: + description = @"Unsupported weave protocol version."; + break; + case GNSErrorUnexpectedWeaveControlPacket: + description = @"Unexpected weave control packet."; + break; + case GNSErrorParsingWeavePacket: + description = @"Parsing weave packet error."; + break; + case GNSErrorWrongWeavePacketCounter: + description = @"Wrong weave packet counter."; + break; + case GNSErrorWeaveDataTransferInProgress: + description = @"Weave data transfer in progress."; + break; + case GNSErrorParsingWeavePacketTooSmall: + description = @"Weave packet too small."; + break; + case GNSErrorParsingWeavePacketTooLarge: + description = @"Weave packet too large."; + break; + case GNSErrorConnectionTimedOut: + description = @"Connection timed out."; + break; + } + NSCAssert(description, @"Unknown NearbySocket error code %ld", (long)errorCode); + NSDictionary<NSErrorUserInfoKey, id> *userInfo = @{NSLocalizedDescriptionKey : description}; + return [NSError errorWithDomain:kGNSSocketsErrorDomain code:errorCode userInfo:userInfo]; +} + +NSString *GNSCharacteristicName(NSString *uuid) { + if ([uuid isEqual:kGNSWeaveToPeripheralCharUUIDString]) { + return @"ToPeripheralChar"; + } else if ([uuid isEqual:kGNSWeaveFromPeripheralCharUUIDString]) { + return @"FromPeripheralChar"; + } else if ([uuid isEqual:kGNSPairingCharUUIDString]) { + return @"PairingChar"; + } + return @"UnknownChar"; +} + +NS_ASSUME_NONNULL_END diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSWeavePacket.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSWeavePacket.h new file mode 100644 index 00000000000..2138c774452 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSWeavePacket.h @@ -0,0 +1,242 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@class GNSWeaveConnectionRequestPacket; +@class GNSWeaveConnectionConfirmPacket; +@class GNSWeaveErrorPacket; +@class GNSWeaveDataPacket; + +extern const UInt8 kGNSMaxPacketCounterValue; +extern const UInt16 kGNSMinSupportedPacketSize; +extern const NSUInteger kGNSMaxCentralHandshakeDataSize; + +typedef NS_ENUM(UInt8, GNSWeaveControlCommand) { + GNSWeaveControlCommandConnectionRequest = 0, + GNSWeaveControlCommandConnectionConfirm = 1, + GNSWeaveControlCommandError = 2, +}; + +/** + * This protocol should be implemented by classes handling Weave BLE packets. The classes should + * implement only the methods corresponding to the packets it should handle. It should be used with + * +[GNSWeavePacket parsePacket:] and -[GNSWeavePacket visitWithHandler:context:] to parse + * serialized Weave packets. + * + * For example, the |Socket| class below handles a serialized weave connection request packet: + * + * @interface Socket : NSObject<GNSWeavePacketHandler> + * @end + * + * @implementation Socket + * + * - (void)didReceivedData:(NSData *)data { + * GNSWeavePacket *packet = [GNSWeavePacket parseData:data error:nil]; + * if ([packet visitWithHandler:self context:nil]) { + * NSLog(@"This class can handle this packet."); + * } else { + * NSLog(@"Unexpected packet received."); + * } + * } + * + * - (void)handleConnectionRequestPacket:(GNSWeaveConnectionRequestPacket *)packet + * context:(nullable id)context { + * NSLog(@"Connection request packet received."); + * ... + * } + * @end + * + **/ +@protocol GNSWeavePacketHandler<NSObject> +@optional + +- (void)handleConnectionRequestPacket:(GNSWeaveConnectionRequestPacket *)packet + context:(nullable id)context; +- (void)handleConnectionConfirmPacket:(GNSWeaveConnectionConfirmPacket *)packet + context:(nullable id)context; +- (void)handleErrorPacket:(GNSWeaveErrorPacket *)packet context:(nullable id)context; +- (void)handleDataPacket:(GNSWeaveDataPacket *)packet context:(nullable id)context; + +@end + +/** + * The Weave BLE protocol (go/weave-ble-gatt-transport) has two types of packets: control and data. + * + * There are 3 types of control packets: + * - connection request (GNSWeaveConnectionRequestPacket); + * - connection confirm (GNSWeaveConnectionConfirmPacket); + * - error (GNSWeaveErrorPacket). + * + * The first two messages are used to establish the Weave BLE logical connection: the central + * (client) sends a connection request packet and the peripheral replies with a connection confirm + * request. This first two messages are used to negociate to connection paramenter: protocol version + * and packet size. + * + * After the logical connection is established the peers can exchange arbitrarily large messages + * that split are into data packets (GNSWeaveDataPacket). + * + * All packets have a 3-bit packet counter. This is used to detect packet drops, re-ordering or + * duplication. There is no recovery strategy, if a peer detects any error it sends an error packet + * to the other peer and closes the connection. + **/ +@interface GNSWeavePacket : NSObject +@property(nonatomic, readonly) UInt8 packetCounter; + +/** + * Parses |data| and, if possible, extracts and return the corresponding Weave packet. It returns + * nil if there was an error parsing the packet. See GNSWeavePacketHandler. + * + * @param data The binary data containing. + * @param outError The error causing the parsing to fail. + * + * @return The corresponding Weave packet or an nil if there was an error. + **/ ++ (nullable GNSWeavePacket *)parseData:(NSData *)data + error:(out __autoreleasing NSError **)outError; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Calls the |handler| method corresponding to the type of the current packet. See + * GNSWeavePacketHandler. + * + * @param handler The packet handler. + * @param context The context passed to the handler. + * + * @return YES if |handler| can handle the current message. + */ +- (BOOL)visitWithHandler:(id<GNSWeavePacketHandler>)handler context:(nullable id)context; + +/** + * Serialize the packet. + */ +- (NSData *)serialize; + +@end + +@interface GNSWeaveConnectionRequestPacket : GNSWeavePacket + +@property(nonatomic, readonly) UInt16 minVersion; +@property(nonatomic, readonly) UInt16 maxVersion; +@property(nonatomic, readonly) UInt16 maxPacketSize; +@property(nonatomic, readonly) NSData *data; + +/** + * Creates an instance of the GNSWeaveConnectionRequestPacket. Note: there is no packet counter + * parameter as this is necessarily the first packet sent by this peer (central/client). + * + * @param minVersion The minimum Weave BLE protocol version supported by this peer (central/client). + * @param maxVersion The maximum Weave BLE protocol version supported by this peer. + * @param maxPacketSize The maximum packet size (in bytes) supported by this peer. According to the + * BLE specs this should be at least 20 and at most 509 bytes. + * @param data The optional data send with the connection request packet. This should not exceed + * 13 bytes + * + * @return GNSWeaveConnectionRequestPacket instance. + **/ +- (nullable instancetype)initWithMinVersion:(UInt16)minVersion + maxVersion:(UInt16)maxVersion + maxPacketSize:(UInt16)maxPacketSize + data:(nullable NSData *)data NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithPacketCounter:(UInt8)packetCounter NS_UNAVAILABLE; + +@end + +@interface GNSWeaveConnectionConfirmPacket : GNSWeavePacket + +@property(nonatomic, readonly) UInt16 version; +@property(nonatomic, readonly) UInt16 packetSize; +@property(nonatomic, readonly) NSData *data; + +/** + * Creates an instance of the GNSWeaveConnectionConfirmPacket. Note: there is no packet counter + * parameter as this is necessarily the first packet sent by this peer (peripheral/server). + * + * @param version The chosen Weave BLE protocol version for the current connection. + * @param packetSize The chosen packet size for the current connection. According to the BLE specs + * this should be at least 20 and at most 509 bytes. + * @param data The optional data send with the connection confirm packet. This should not exceed 15 + * bytes. + * + * @return GNSWeaveConnectionConfirmPacket instance. + **/ +- (nullable instancetype)initWithVersion:(UInt16)version + packetSize:(UInt16)packetSize + data:(nullable NSData *)data NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithPacketCounter:(UInt8)packetCounter NS_UNAVAILABLE; + +@end + +@interface GNSWeaveErrorPacket : GNSWeavePacket + +/** + * Creates an instance of the GNSWeaveErrorPacket. + * + * @param packetCounter The current 3-bit packet counter (i.e. should be stricly smaller than 8). + * + * @return GNSWeaveErrorPacket instance. + **/ +- (nullable instancetype)initWithPacketCounter:(UInt8)packetCounter NS_DESIGNATED_INITIALIZER; + +@end + +@interface GNSWeaveDataPacket : GNSWeavePacket +@property(nonatomic, readonly, getter=isFirstPacket) BOOL firstPacket; +@property(nonatomic, readonly, getter=isLastPacket) BOOL lastPacket; +@property(nonatomic, readonly) NSData *data; + +/** + * Creates an instance of the GNSWeaveDataPacket for |data| starting at |outOffset| containing at + * most |packetSize|-1 bytes, and updates |outOffset| for the next packet. This should be used + * iteratively to split a message in GNSWeaveDataPacket's to be send to other peer. + * + * @param packetCounter The current 3-bit packet counter (i.e. should be stricly smaller than 8). + * @param packetSize The maximum size of a data packet (including the 1-byte header). + * @param data The data to send to the other peer. + * @param inOutOffset The offset for |data|. It's updated with the new offset value for the next + * data packet. It's equal to |data.length| if this is the last packet. + * + * @return GNSWeaveDataPacket instance. + **/ ++ (nullable GNSWeaveDataPacket *)dataPacketWithPacketCounter:(UInt8)packetCounter + packetSize:(UInt16)packetSize + data:(NSData *)data + offset:(NSUInteger *)inOutOffset; + +/** + * Creates an instance of the GNSWeaveDataPacket. Avoid using this initializer directly, prefer + * using -[GNSWeaveDataPacket dataPacketWithPacketCounter:packetSize:data:offset:]. + * + * @param packetCounter The current 3-bit packet counter (i.e. should be stricly smaller than 8). + * @param isFirstPacket YES if this is the first packet of a message. + * @param isLastPacket YES if this is the last packet of a message. + * @param data The actual data being send. + * + * @return GNSWeaveDataPacket instance. + **/ +- (nullable instancetype)initWithPacketCounter:(UInt8)packetCounter + firstPacket:(BOOL)isFirstPacket + lastPacket:(BOOL)isLastPacket + data:(NSData *)data NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithPacketCounter:(UInt8)packetCounter NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSWeavePacket.m b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSWeavePacket.m new file mode 100644 index 00000000000..5244dc052ea --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSWeavePacket.m @@ -0,0 +1,416 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSWeavePacket.h" + +#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils+Private.h" + +// Constants defined by the Weave BLE protocol. +const UInt8 kGNSMaxPacketCounterValue = 8; +const UInt16 kGNSMinSupportedPacketSize = 20; +const NSUInteger kGNSMaxCentralHandshakeDataSize = 13; +static const NSUInteger kHeaderSize = 1; +static const NSUInteger kMaxControlPacketSize = 20; +static const NSUInteger kMinConnectionRequestPayloadSize = 6; +static const NSUInteger kMinConnectionConfirmPayloadSize = 4; + +// Header offsets. +static const UInt8 kPacketTypeOffset = 7; +static const UInt8 kPacketCounterOffset = 4; +static const UInt8 kFirstPacketFlagOffset = 3; +static const UInt8 kLastPacketFlagOffset = 2; + +// Bitmasks for the packet headers. +static const UInt8 kPacketTypeBitmask = (1 << 7); // 10000000 +static const UInt8 kPacketCounterBitmask = (1 << 6) | (1 << 5) | (1 << 4); // 01110000 +static const UInt8 kControlCommandBitmask = (1 << 3) | (1 << 2) | (1 << 1) | (1 << 0); // 00001111 +static const UInt8 kFirstPacketFlagBitmask = (1 << 3); // 00001000 +static const UInt8 kLastPacketFlagBitmask = (1 << 2); // 00000100 + +static const UInt8 kControlPacketValue = 1; + +/** + * The Weave BLE protocol (go/weave-ble-gatt-transport) has two types of packets: control and data. + * Both packets contain a 1-byte header followed by a variable size payload. The header + * bit-structure is the following: + * + * 1) Control Packet: TCCCNNNN + * T: packet type, 1 for control packets + * CCC: packet counter + * NNNN: control command number (GNSWeaveControlCommand) + * + * 2) Data Packet: TCCCFL00 + * T: packet type, 0 for data packets + * CCC: packet counter + * F: bit indicating the first packet of a message + * L: bit indication the last packet of a message + * + * Note: A single packet message will have both F and L set. + * + * The helpers below are used to manipulate the headers. + **/ +static UInt8 ExtractPacketType(UInt8 header) { + return (header & kPacketTypeBitmask) >> kPacketTypeOffset; +} + +static UInt8 ExtractPacketCounter(UInt8 header) { + return (header & kPacketCounterBitmask) >> kPacketCounterOffset; +} + +static UInt8 ExtractControlCommand(UInt8 header) { return (header & kControlCommandBitmask); } + +static UInt8 ExtractFirstPacketFlag(UInt8 header) { + return (header & kFirstPacketFlagBitmask) >> kFirstPacketFlagOffset; +} + +static UInt8 ExtractLastPacketFlag(UInt8 header) { + return (header & kLastPacketFlagBitmask) >> kLastPacketFlagOffset; +} + +// The Weave protocol uses big-endian format (network byte order) for multi-byte types. +static UInt16 ExtractUInt16(const void *bytes, size_t offset) { + return CFSwapInt16BigToHost(*(UInt16 *)(bytes + offset)); +} + +static NSData *ExtractPayloadData(NSData *payload, size_t offset) { + if (offset >= payload.length) { + return nil; + } + return [payload subdataWithRange:NSMakeRange(offset, payload.length - offset)]; +} + +static UInt8 WeaveControlPacketHeader(UInt8 packetCounter, GNSWeaveControlCommand command) { + return (1 << kPacketTypeOffset) | (packetCounter << kPacketCounterOffset) | command; +} + +static UInt8 WeaveDataPacketHeader(UInt8 packetCounter, BOOL firstPacketFlag, BOOL lastPacketFlag) { + return (packetCounter << kPacketCounterOffset) | (firstPacketFlag << kFirstPacketFlagOffset) | + (lastPacketFlag << kLastPacketFlagOffset); +} + +@interface GNSWeavePacket () + +- (nullable instancetype)initWithPacketCounter:(UInt8)packetCounter NS_DESIGNATED_INITIALIZER; + +@end + +@implementation GNSWeavePacket + ++ (GNSWeavePacket *)parseData:(NSData *)data error:(out __autoreleasing NSError **)outError { + if (data.length < kHeaderSize) { + if (outError) { + *outError = GNSErrorWithCode(GNSErrorParsingWeavePacketTooSmall); + } + return nil; + } + // The header is the first byte of |data|. + UInt8 header = *(UInt8 *)data.bytes; + BOOL isControlPacket = ExtractPacketType(header) == kControlPacketValue; + UInt8 packetCounter = ExtractPacketCounter(header); + + GNSWeavePacket *parsedPacket = nil; + if (isControlPacket) { + if (data.length > kMaxControlPacketSize) { + if (outError) { + *outError = GNSErrorWithCode(GNSErrorParsingWeavePacketTooLarge); + } + return nil; + } + GNSWeaveControlCommand controlCommand = ExtractControlCommand(header); + switch (controlCommand) { + case GNSWeaveControlCommandConnectionRequest: { + NSData *payload = [data subdataWithRange:NSMakeRange(1, data.length - 1)]; + if (payload.length < kMinConnectionRequestPayloadSize) { + if (outError) { + *outError = GNSErrorWithCode(GNSErrorParsingWeavePacketTooSmall); + } + return nil; + } + UInt16 minVersion = ExtractUInt16(payload.bytes, 0); + UInt16 maxVersion = ExtractUInt16(payload.bytes, sizeof(minVersion)); + UInt16 maxPacketSize = + ExtractUInt16(payload.bytes, sizeof(minVersion) + sizeof(maxVersion)); + + // Extracting the (optional) payload data. + size_t payloadDataOffset = sizeof(maxVersion) + sizeof(minVersion) + sizeof(maxPacketSize); + NSData *payloadData = ExtractPayloadData(payload, payloadDataOffset); + parsedPacket = [[GNSWeaveConnectionRequestPacket alloc] initWithMinVersion:minVersion + maxVersion:maxVersion + maxPacketSize:maxPacketSize + data:payloadData]; + break; + } + case GNSWeaveControlCommandConnectionConfirm: { + NSData *payload = [data subdataWithRange:NSMakeRange(1, data.length - 1)]; + if (payload.length < kMinConnectionConfirmPayloadSize) { + if (outError) { + *outError = GNSErrorWithCode(GNSErrorParsingWeavePacketTooSmall); + } + return nil; + } + UInt16 version = ExtractUInt16(payload.bytes, 0); + UInt16 packetSize = ExtractUInt16(payload.bytes, sizeof(version)); + size_t payloadDataOffset = sizeof(version) + sizeof(packetSize); + + // Extracting the (optional) payload data. + NSData *payloadData = ExtractPayloadData(payload, payloadDataOffset); + parsedPacket = [[GNSWeaveConnectionConfirmPacket alloc] initWithVersion:version + packetSize:packetSize + data:payloadData]; + break; + } + case GNSWeaveControlCommandError: { + parsedPacket = [[GNSWeaveErrorPacket alloc] initWithPacketCounter:packetCounter]; + break; + } + } + } else { + parsedPacket = [[GNSWeaveDataPacket alloc] + initWithPacketCounter:packetCounter + firstPacket:ExtractFirstPacketFlag(header) + lastPacket:ExtractLastPacketFlag(header) + data:[data subdataWithRange:NSMakeRange(1, data.length - 1)]]; + } + NSAssert(parsedPacket != nil, @"Parsed packet cannot be nil."); + return parsedPacket; +} + +- (instancetype)initWithPacketCounter:(UInt8)packetCounter { + NSAssert(packetCounter < kGNSMaxPacketCounterValue, + @"The packetCounter should have at most 3 bits."); + self = [super init]; + if (self) { + _packetCounter = packetCounter; + } + return self; +} + +- (instancetype)init { + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (BOOL)visitWithHandler:(id<GNSWeavePacketHandler>)handler context:(id)context { + [self doesNotRecognizeSelector:_cmd]; + return NO; +} + +- (NSData *)serialize { + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"<%@: %p, packet counter: %d>", [self class], self, _packetCounter]; +} + +@end + +@implementation GNSWeaveConnectionRequestPacket + +- (instancetype)initWithMinVersion:(UInt16)minVersion + maxVersion:(UInt16)maxVersion + maxPacketSize:(UInt16)maxPacketSize + data:(NSData *)data { + self = [super initWithPacketCounter:0]; + if (self) { + _minVersion = minVersion; + _maxVersion = maxVersion; + _maxPacketSize = maxPacketSize; + _data = data; + } + return self; +} + +- (instancetype)initWithPacketCounter:(UInt8)packetCounter { + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (BOOL)visitWithHandler:(id<GNSWeavePacketHandler>)handler context:(id)context { + if ([handler respondsToSelector:@selector(handleConnectionRequestPacket:context:)]) { + [handler handleConnectionRequestPacket:self context:context]; + return YES; + } + return NO; +} + +- (NSData *)serialize { + NSMutableData *packet = [NSMutableData data]; + // The Weave protocol uses big-endian format (network byte order) for multi-byte types. + UInt16 minVersionBigEndian = CFSwapInt16HostToBig(self.minVersion); + UInt16 maxVersionBigEndian = CFSwapInt16HostToBig(self.maxVersion); + UInt16 maxPacketSizeBigEndian = CFSwapInt16HostToBig(self.maxPacketSize); + UInt8 header = WeaveControlPacketHeader(0, GNSWeaveControlCommandConnectionRequest); + [packet appendBytes:&header length:sizeof(header)]; + [packet appendBytes:&minVersionBigEndian length:sizeof(minVersionBigEndian)]; + [packet appendBytes:&maxVersionBigEndian length:sizeof(maxVersionBigEndian)]; + [packet appendBytes:&maxPacketSizeBigEndian length:sizeof(maxPacketSizeBigEndian)]; + [packet appendData:self.data]; + NSAssert(packet.length <= kMaxControlPacketSize, @"Control packets cannot be larger than %lu", + (unsigned long)kMaxControlPacketSize); + return packet; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p, packet counter: %d, min version: %d, max version: " + @"%d, max packet size: %d, data size: %ld>", + [self class], self, self.packetCounter, _minVersion, + _maxVersion, _maxPacketSize, (unsigned long)_data.length]; +} + +@end + +@implementation GNSWeaveConnectionConfirmPacket + +- (instancetype)initWithVersion:(UInt16)version packetSize:(UInt16)packetSize data:(NSData *)data { + NSAssert(packetSize >= kGNSMinSupportedPacketSize, @"The minimum packet size is %ld", + (long)kGNSMinSupportedPacketSize); + self = [super initWithPacketCounter:0]; + if (self) { + _version = version; + _packetSize = packetSize; + _data = data; + } + return self; +} + +- (instancetype)initWithPacketCounter:(UInt8)packetCounter { + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (BOOL)visitWithHandler:(id<GNSWeavePacketHandler>)handler context:(id)context { + if ([handler respondsToSelector:@selector(handleConnectionConfirmPacket:context:)]) { + [handler handleConnectionConfirmPacket:self context:context]; + return YES; + } + return NO; +} + +- (NSData *)serialize { + NSMutableData *packet = [NSMutableData data]; + // The Weave protocol uses big-endian format (network byte order) encoding for multi-byte types. + UInt16 versionBigEndian = CFSwapInt16HostToBig(self.version); + UInt16 packetSizeBigEndian = CFSwapInt16HostToBig(self.packetSize); + UInt8 header = WeaveControlPacketHeader(0, GNSWeaveControlCommandConnectionConfirm); + [packet appendBytes:&header length:sizeof(header)]; + [packet appendBytes:&versionBigEndian length:sizeof(versionBigEndian)]; + [packet appendBytes:&packetSizeBigEndian length:sizeof(packetSizeBigEndian)]; + [packet appendData:self.data]; + NSAssert(packet.length <= kMaxControlPacketSize, @"Control packets cannot be larger than %lu", + (unsigned long)kMaxControlPacketSize); + return packet; +} + +- (NSString *)description { + return + [NSString stringWithFormat: + @"<%@: %p, packet counter: %d, version: %d, packet size: %d, data size: %ld>", + [self class], self, self.packetCounter, _version, _packetSize, + (unsigned long)_data.length]; +} + +@end + +@implementation GNSWeaveErrorPacket + +- (instancetype)initWithPacketCounter:(UInt8)packetCounter { + return [super initWithPacketCounter:packetCounter]; +} + +- (BOOL)visitWithHandler:(id<GNSWeavePacketHandler>)handler context:(id)context { + if ([handler respondsToSelector:@selector(handleErrorPacket:context:)]) { + [handler handleErrorPacket:self context:context]; + return YES; + } + return NO; +} + +- (NSData *)serialize { + NSMutableData *packet = [NSMutableData data]; + UInt8 header = WeaveControlPacketHeader(self.packetCounter, GNSWeaveControlCommandError); + [packet appendBytes:&header length:sizeof(header)]; + return packet; +} + +@end + +@implementation GNSWeaveDataPacket + ++ (nullable GNSWeaveDataPacket *)dataPacketWithPacketCounter:(UInt8)packetCounter + packetSize:(UInt16)packetSize + data:(NSData *)data + offset:(NSUInteger *)outOffset { + NSAssert(packetCounter < kGNSMaxPacketCounterValue, @"The packet has more than 3 bits."); + NSAssert(data.length == 0 || *outOffset < data.length, + @"The offset falls outside of the data range, data length: %ld, outOffset: %ld", + (unsigned long)data.length, (unsigned long)*outOffset); + + BOOL isFirstPacket = (*outOffset == 0); + NSInteger dataSize = MIN(packetSize - kHeaderSize, data.length - *outOffset); + BOOL isLastPacket = (data.length <= (*outOffset + dataSize)); + NSData *packetData = [data subdataWithRange:NSMakeRange(*outOffset, dataSize)]; + *outOffset = *outOffset + dataSize; + + return [[GNSWeaveDataPacket alloc] initWithPacketCounter:packetCounter + firstPacket:isFirstPacket + lastPacket:isLastPacket + data:packetData]; +} + +- (instancetype)initWithPacketCounter:(UInt8)packetCounter + firstPacket:(BOOL)isFirstPacket + lastPacket:(BOOL)isLastPacket + data:(nonnull NSData *)data { + self = [super initWithPacketCounter:packetCounter]; + if (self) { + _firstPacket = isFirstPacket; + _lastPacket = isLastPacket; + _data = data; + } + return self; +} + +- (instancetype)initWithPacketCounter:(UInt8)packetCounter { + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (BOOL)visitWithHandler:(id<GNSWeavePacketHandler>)handler context:(id)context { + if ([handler respondsToSelector:@selector(handleDataPacket:context:)]) { + [handler handleDataPacket:self context:context]; + return YES; + } + return NO; +} + +- (NSData *)serialize { + UInt8 header = WeaveDataPacketHeader(self.packetCounter, self.isFirstPacket, self.isLastPacket); + NSMutableData *packet = [NSMutableData data]; + [packet appendBytes:&header length:sizeof(header)]; + [packet appendData:self.data]; + return packet; +} + +- (NSString *)description { + return [NSString + stringWithFormat: + @"<%@: %p, packet counter: %d, first packet: %@, last packet: %@, data length: %ld>", + [self class], self, self.packetCounter, _firstPacket ? @"YES" : @"NO", + _lastPacket ? @"YES" : @"NO", (unsigned long)_data.length]; +} + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Central/GNSCentralManagerTest.m b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Central/GNSCentralManagerTest.m new file mode 100644 index 00000000000..725b54745e1 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Central/GNSCentralManagerTest.m @@ -0,0 +1,299 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import <XCTest/XCTest.h> + +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Central/GNSCentralManager+Private.h" +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Central/GNSCentralPeerManager+Private.h" +#import "third_party/objective_c/ocmock/v3/Source/OCMock/OCMock.h" + +@interface TestGNSCentralManager : GNSCentralManager +@end + +@implementation TestGNSCentralManager + ++ (CBCentralManager *)centralManagerWithDelegate:(id<CBCentralManagerDelegate>)delegate + queue:(dispatch_queue_t)queue + options:(NSDictionary *)options { + CBCentralManager *result = OCMStrictClassMock([CBCentralManager class]); + OCMStub([result delegate]).andReturn(delegate); + return result; +} + +- (GNSCentralPeerManager *)createCentralPeerManagerWithPeripheral:(CBPeripheral *)peripheral { + GNSCentralPeerManager *manager = OCMStrictClassMock([GNSCentralPeerManager class]); + OCMStub([manager cbPeripheral]).andReturn(peripheral); + return manager; +} + +@end + +@interface GNSCentralManagerTest : XCTestCase { + CBUUID *_socketServiceUUID; + TestGNSCentralManager *_centralManager; + CBCentralManager *_cbCentralManagerMock; + CBCentralManagerState _cbCentralManagerState; + NSMutableArray *_mockObjectsToVerify; + id<GNSCentralManagerDelegate> _centralManagerDelegate; +} +@end + +@implementation GNSCentralManagerTest + +- (void)setUp { + _mockObjectsToVerify = [NSMutableArray array]; + _socketServiceUUID = [CBUUID UUIDWithNSUUID:[NSUUID UUID]]; + _centralManager = [[TestGNSCentralManager alloc] + initWithSocketServiceUUID:_socketServiceUUID + queue:dispatch_get_main_queue()]; + _centralManagerDelegate = OCMStrictProtocolMock(@protocol(GNSCentralManagerDelegate)); + _centralManager.delegate = _centralManagerDelegate; + XCTAssertFalse(_centralManager.scanning); + XCTAssertEqualObjects(_centralManager.socketServiceUUID, _socketServiceUUID); + _cbCentralManagerMock = _centralManager.testing_cbCentralManager; + XCTAssertEqual(_cbCentralManagerMock.delegate, _centralManager); + _cbCentralManagerState = CBCentralManagerStatePoweredOff; + OCMStub([_cbCentralManagerMock state]) + .andDo(^(NSInvocation *invocation) { + [invocation setReturnValue:&_cbCentralManagerState]; + }); +} + +- (void)tearDown { + OCMVerifyAll((id)_cbCentralManagerMock); + OCMVerifyAll((id)_centralManagerDelegate); + for (OCMockObject *object in _mockObjectsToVerify) { + OCMVerifyAll(object); + } +} + +#pragma mark - Scanning And Power Off/On Bluetooth + +- (void)startScanningWithServices:(NSArray *)services + advertismentName:(NSString *)advertismentName { + NSDictionary *options = @{ CBCentralManagerScanOptionAllowDuplicatesKey : @YES }; + OCMExpect([_cbCentralManagerMock scanForPeripheralsWithServices:services options:options]); + [_centralManager startScanWithAdvertisedName:advertismentName + advertisedServiceUUID:_centralManager.socketServiceUUID]; + XCTAssertTrue(_centralManager.scanning); +} + +- (void)testScanningWithBluetoothOFF { + [_centralManager startScanWithAdvertisedName:nil + advertisedServiceUUID:_centralManager.socketServiceUUID]; + XCTAssertTrue(_centralManager.scanning); + [_centralManager stopScan]; + XCTAssertFalse(_centralManager.scanning); +} + +- (void)testScanningWithBluetoothON { + _cbCentralManagerState = CBCentralManagerStatePoweredOn; + NSArray *services = @[ _socketServiceUUID ]; + [self startScanningWithServices:services advertismentName:nil]; + OCMExpect([_cbCentralManagerMock stopScan]); + [_centralManager stopScan]; + XCTAssertFalse(_centralManager.scanning); +} + +- (void)testScanningAndPowerONBluetooth { + NSArray *services = @[ _socketServiceUUID ]; + [self startScanningWithServices:services advertismentName:nil]; + OCMExpect([_centralManagerDelegate centralManagerDidUpdateBLEState:_centralManager]); + _cbCentralManagerState = CBCentralManagerStatePoweredOn; + [_centralManager centralManagerDidUpdateState:_cbCentralManagerMock]; + XCTAssertTrue(_centralManager.scanning); + OCMExpect([_cbCentralManagerMock stopScan]); + [_centralManager stopScan]; + XCTAssertFalse(_centralManager.scanning); +} + +- (void)testScanningAndPowerOFFBluetooth { + _cbCentralManagerState = CBCentralManagerStatePoweredOn; + NSArray *services = @[ _socketServiceUUID ]; + [self startScanningWithServices:services advertismentName:nil]; + OCMExpect([_centralManagerDelegate centralManagerDidUpdateBLEState:_centralManager]); + OCMExpect([_cbCentralManagerMock stopScan]); + _cbCentralManagerState = CBCentralManagerStatePoweredOff; + [_centralManager centralManagerDidUpdateState:_cbCentralManagerMock]; + XCTAssertTrue(_centralManager.scanning); + [_centralManager stopScan]; + XCTAssertFalse(_centralManager.scanning); +} + +- (void)testScanningWithAdvertisedName { + _cbCentralManagerState = CBCentralManagerStatePoweredOn; + [self startScanningWithServices:nil advertismentName:@"advertisedname"]; + OCMExpect([_cbCentralManagerMock stopScan]); + [_centralManager stopScan]; + XCTAssertFalse(_centralManager.scanning); +} + +#pragma mark - Scanning with no advertised name + +- (void)testScanningPeripheralWithWrongPeripheralService { + _cbCentralManagerState = CBCentralManagerStatePoweredOn; + [self startScanningWithServices:@[ _socketServiceUUID ] advertismentName:nil]; + CBPeripheral *peripheral = OCMStrictClassMock([CBPeripheral class]); + NSDictionary *advertisementData = @{ CBAdvertisementDataServiceUUIDsKey: @[ [NSUUID UUID] ] }; + [_centralManager centralManager:_cbCentralManagerMock + didDiscoverPeripheral:peripheral + advertisementData:advertisementData + RSSI:@(19)]; +} + +- (void)testScanningPeripheral { + _cbCentralManagerState = CBCentralManagerStatePoweredOn; + [self startScanningWithServices:@[ _socketServiceUUID ] advertismentName:nil]; + CBPeripheral *peripheral = OCMStrictClassMock([CBPeripheral class]); + OCMStub([peripheral identifier]).andReturn([NSUUID UUID]); + NSDictionary *advertisementData = @{ + CBAdvertisementDataServiceUUIDsKey : @[ _socketServiceUUID ] + }; + OCMExpect([_centralManagerDelegate + centralManager:_centralManager + didDiscoverPeer:[OCMArg checkWithBlock:^BOOL(GNSCentralPeerManager *centralPeerManager) { + XCTAssertEqual(centralPeerManager.cbPeripheral, peripheral); + return YES; + }] + advertisementData:[OCMArg any]]); + [_centralManager centralManager:_cbCentralManagerMock + didDiscoverPeripheral:peripheral + advertisementData:advertisementData + RSSI:@(42)]; +} + +#pragma mark - Scanning with advertised name + +- (void)testScanningPeripheralWithWrongAdvertisedName { + _cbCentralManagerState = CBCentralManagerStatePoweredOn; + [self startScanningWithServices:nil advertismentName:@"advertisedname"]; + CBPeripheral *peripheral = OCMStrictClassMock([CBPeripheral class]); + NSDictionary *advertisementData = @{ + CBAdvertisementDataServiceUUIDsKey : @[ [NSUUID UUID] ], + CBAdvertisementDataLocalNameKey : @"wrongadvertisedname", + }; + [_centralManager centralManager:_cbCentralManagerMock + didDiscoverPeripheral:peripheral + advertisementData:advertisementData + RSSI:@(19)]; +} + +- (void)testScanningPeripheralWithRightAdvertisedName { + _cbCentralManagerState = CBCentralManagerStatePoweredOn; + NSString *advertisedName = @"advertisedname"; + [self startScanningWithServices:nil advertismentName:advertisedName]; + CBPeripheral *peripheral = OCMStrictClassMock([CBPeripheral class]); + OCMStub([peripheral identifier]).andReturn([NSUUID UUID]); + NSDictionary *advertisementData = @{ + CBAdvertisementDataServiceUUIDsKey : @[ _socketServiceUUID ], + CBAdvertisementDataLocalNameKey : advertisedName, + }; + OCMExpect([_centralManagerDelegate centralManager:_centralManager + didDiscoverPeer:[OCMArg any] + advertisementData:[OCMArg any]]); + [_centralManager centralManager:_cbCentralManagerMock + didDiscoverPeripheral:peripheral + advertisementData:advertisementData + RSSI:@(19)]; +} + +#pragma mark - Peripheral Retrieval + +- (CBPeripheral *)prepareCBPeripheralWithIdentifier:(NSUUID *)identifier + peripheralState:(CBPeripheralState *)peripheralState { + CBPeripheral *peripheral = OCMStrictClassMock([CBPeripheral class]); + OCMStub([peripheral identifier]).andReturn(identifier); + OCMStub([peripheral state]) + .andDo(^(NSInvocation *invocation) { + [invocation setReturnValue:peripheralState]; + }); + [_mockObjectsToVerify addObject:(OCMockObject *)peripheral]; + return peripheral; +} + +- (GNSCentralPeerManager *)retrieveCentralPeerWithPeripheral:(CBPeripheral *)peripheral { + NSUUID *identifier = peripheral.identifier; + OCMExpect([_cbCentralManagerMock retrievePeripheralsWithIdentifiers:@[ identifier ]]) + .andReturn([NSArray arrayWithObject:peripheral]); + GNSCentralPeerManager *centralPeerManager = + [_centralManager retrieveCentralPeerWithIdentifier:identifier]; + OCMStub([centralPeerManager cbPeripheral]).andReturn(peripheral); + OCMStub([centralPeerManager identifier]).andReturn(identifier); + XCTAssertNotNil(centralPeerManager); + [_mockObjectsToVerify addObject:(OCMockObject *)centralPeerManager]; + return centralPeerManager; +} + +- (void)testRetrieveCentralPeerAndConnectDisconnect { + _cbCentralManagerState = CBCentralManagerStatePoweredOn; + NSUUID *identifier = [NSUUID UUID]; + CBPeripheralState peripheralMockState = CBPeripheralStateConnected; + CBPeripheral *peripheral = + [self prepareCBPeripheralWithIdentifier:identifier peripheralState:&peripheralMockState]; + GNSCentralPeerManager *centralPeerManager = [self retrieveCentralPeerWithPeripheral:peripheral]; + OCMExpect([centralPeerManager bleConnected]); + [_centralManager centralManager:_cbCentralManagerMock didConnectPeripheral:peripheral]; + NSError *error = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + OCMExpect([centralPeerManager bleDisconnectedWithError:error]); + peripheralMockState = CBPeripheralStateDisconnected; + [_centralManager centralManager:_cbCentralManagerMock + didDisconnectPeripheral:peripheral + error:error]; +} + +- (void)testRetrieveTwiceCentralPeer { + _cbCentralManagerState = CBCentralManagerStatePoweredOn; + NSUUID *identifier = [NSUUID UUID]; + CBPeripheralState peripheralMockState = CBPeripheralStateConnected; + CBPeripheral *peripheral = + [self prepareCBPeripheralWithIdentifier:identifier peripheralState:&peripheralMockState]; + GNSCentralPeerManager *centralPeerManager = [self retrieveCentralPeerWithPeripheral:peripheral]; + XCTAssertNil([_centralManager retrieveCentralPeerWithIdentifier:identifier]); + OCMExpect([centralPeerManager bleConnected]); + [_centralManager centralManager:_cbCentralManagerMock didConnectPeripheral:peripheral]; + NSError *error = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + OCMExpect([centralPeerManager bleDisconnectedWithError:error]); + peripheralMockState = CBPeripheralStateDisconnected; + [_centralManager centralManager:_cbCentralManagerMock + didDisconnectPeripheral:peripheral + error:error]; + [_centralManager centralPeerManagerDidDisconnect:centralPeerManager]; + + NSUUID *identifier2 = [NSUUID UUID]; + CBPeripheralState peripheralState2 = CBPeripheralStateConnected; + CBPeripheral *peripheral2 = + [self prepareCBPeripheralWithIdentifier:identifier2 peripheralState:&peripheralState2]; + GNSCentralPeerManager *centralPeerManager2 = [self retrieveCentralPeerWithPeripheral:peripheral2]; + XCTAssertNotNil(centralPeerManager2); + XCTAssertNotEqual(centralPeerManager, centralPeerManager2); +} + +- (void)testRetrieveCentralPeerAndConnectFailToConnect { + _cbCentralManagerState = CBCentralManagerStatePoweredOn; + NSUUID *identifier = [NSUUID UUID]; + CBPeripheralState peripheralMockState = CBPeripheralStateConnected; + CBPeripheral *peripheral = + [self prepareCBPeripheralWithIdentifier:identifier peripheralState:&peripheralMockState]; + GNSCentralPeerManager *centralPeerManager = [self retrieveCentralPeerWithPeripheral:peripheral]; + OCMExpect([centralPeerManager bleConnected]); + [_centralManager centralManager:_cbCentralManagerMock didConnectPeripheral:peripheral]; + NSError *error = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + OCMExpect([centralPeerManager bleDisconnectedWithError:error]); + peripheralMockState = CBPeripheralStateDisconnected; + [_centralManager centralManager:_cbCentralManagerMock + didFailToConnectPeripheral:peripheral + error:error]; +} + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Central/GNSCentralPeerManagerTest.m b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Central/GNSCentralPeerManagerTest.m new file mode 100644 index 00000000000..17ed74a1d59 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Central/GNSCentralPeerManagerTest.m @@ -0,0 +1,465 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import <XCTest/XCTest.h> + +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Central/GNSCentralManager+Private.h" +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Central/GNSCentralPeerManager+Private.h" +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSWeavePacket.h" +#import "third_party/objective_c/ocmock/v3/Source/OCMock/OCMock.h" + +static SEL gTimerSelector = nil; +static GNSCentralPeerManager *gTimerTarget = nil; + +static void ClearTimer(void) { + gTimerSelector = nil; + gTimerTarget = nil; +} + +static void FireTimer(void) { + NSCAssert(gTimerTarget != nil, @"The timer target cannot be nil."); + NSCAssert(gTimerSelector != nil, @"The timer selector cannot be nil."); + NSInvocation *invocation = [NSInvocation + invocationWithMethodSignature:[[gTimerTarget class] + instanceMethodSignatureForSelector:gTimerSelector]]; + invocation.target = gTimerTarget; + invocation.selector = gTimerSelector; + [invocation invoke]; + ClearTimer(); +} + +@interface TestGNSCentralPeerManager : GNSCentralPeerManager +@end + +@implementation TestGNSCentralPeerManager + ++ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval + target:(id)target + selector:(SEL)selector + userInfo:(nullable id)userInfo + repeats:(BOOL)yesOrNo { + NSAssert(target != nil, @"The timer target cannot be nil."); + NSAssert(selector != nil, @"The timer selector cannot be nil."); + NSAssert(gTimerSelector == nil, @"Timer selector already set."); + NSAssert(gTimerTarget == nil, @"Time target already set."); + gTimerSelector = selector; + gTimerTarget = target; + NSTimer *timer = OCMStrictClassMock([NSTimer class]); + OCMStub([timer timeInterval]).andReturn(timeInterval); + return timer; +} + +@end + +@interface GNSCentralPeerManagerTest : XCTestCase { + TestGNSCentralPeerManager *_centralPeerManager; + CBCentralManagerState _cbCentralManagerState; + GNSCentralManager *_centralManagerMock; + CBUUID *_serviceUUID; + CBPeripheral *_peripheralMock; + CBPeripheralState _peripheralState; + NSUUID *_peripheralUUID; + CBCharacteristic *_toPeripheralCharacteristic; + CBCharacteristic *_fromPeripheralCharacteristic; + CBCharacteristic *_pairingCharacteristic; + NSArray *_characteristics; + NSArray *_characteristicsWithPairing; + CBService *_serviceMock; + NSArray *_services; + GNSSocket *_socket; + id<GNSSocketDelegate> _socketDelegateMock; +} +@end + +@implementation GNSCentralPeerManagerTest + +- (void)setUp { + _peripheralUUID = [NSUUID UUID]; + _serviceUUID = [CBUUID UUIDWithNSUUID:[NSUUID UUID]]; + _centralManagerMock = [OCMStrictClassMock([GNSCentralManager class]) noRetainObjectArgs]; + OCMStub([_centralManagerMock socketServiceUUID]).andReturn(_serviceUUID); + _cbCentralManagerState = CBCentralManagerStatePoweredOn; + OCMStub([_centralManagerMock cbCentralManagerState]) + .andDo(^(NSInvocation *invocation) { + [invocation setReturnValue:&_cbCentralManagerState]; + }); + _peripheralMock = [OCMStrictClassMock([CBPeripheral class]) noRetainObjectArgs]; + OCMStub([_peripheralMock identifier]).andReturn(_peripheralUUID); + _peripheralState = CBPeripheralStateDisconnected; + OCMStub([_peripheralMock state]) + .andDo(^(NSInvocation *invocation) { + [invocation setReturnValue:&_peripheralState]; + }); + OCMExpect([_peripheralMock setDelegate:[OCMArg isNotNil]]); + _centralPeerManager = [[TestGNSCentralPeerManager alloc] initWithPeripheral:_peripheralMock + centralManager:_centralManagerMock]; + void *centralPeerManagerNonRetained = (__bridge void *)(_centralPeerManager); + OCMStub([_peripheralMock delegate]).andReturn(centralPeerManagerNonRetained); + _toPeripheralCharacteristic = + [self generateCharateristicMockWithIdentifierString:@"00000100-0004-1000-8000-001A11000101"]; + _fromPeripheralCharacteristic = + [self generateCharateristicMockWithIdentifierString:@"00000100-0004-1000-8000-001A11000102"]; + _pairingCharacteristic = + [self generateCharateristicMockWithIdentifierString:@"17836FBD-8C6A-4B81-83CE-8560629E834B"]; + _characteristics = @[ _toPeripheralCharacteristic, _fromPeripheralCharacteristic ]; + _characteristicsWithPairing = + @[ _toPeripheralCharacteristic, _fromPeripheralCharacteristic, _pairingCharacteristic ]; + _serviceMock = OCMStrictClassMock([CBService class]); + OCMStub([_serviceMock UUID]).andReturn(_serviceUUID); + _services = @[ _serviceMock ]; + OCMStub([_peripheralMock services]).andReturn(_services); + _socketDelegateMock = OCMStrictProtocolMock(@protocol(GNSSocketDelegate)); +} + +- (void)tearDown { + if (_peripheralState != CBPeripheralStateDisconnected) { + // CBPeripharal must be disconnected when GNSCentralPeerManager is dealocated. OCM retains the + // parameter. So |_centralManagerMock| cannot be set in the paramter in order to be deallocated + // correctly. + void *centralPeerManagerNonRetained = (__bridge void *)(_centralPeerManager); + OCMExpect([_centralManagerMock + cancelPeripheralConnectionForPeer:[OCMArg checkWithBlock:^BOOL(id obj) { + return obj == centralPeerManagerNonRetained; + }]]); + } + _centralPeerManager = nil; + ClearTimer(); + OCMVerifyAll((id)_centralManagerMock); + OCMVerifyAll((id)_peripheralMock); + OCMVerifyAll((id)_toPeripheralCharacteristic); + OCMVerifyAll((id)_fromPeripheralCharacteristic); + OCMVerifyAll((id)_pairingCharacteristic); + OCMVerifyAll((id)_serviceMock); + OCMVerifyAll((id)_socketDelegateMock); + __weak TestGNSCentralPeerManager *weakCentralPeerManager = _centralPeerManager; + _centralPeerManager = nil; + XCTAssertNil(weakCentralPeerManager); +} + +- (NSDictionary *)peripheralConnectionOptions { + return @{ + CBConnectPeripheralOptionNotifyOnDisconnectionKey : @YES, +#if TARGET_OS_IPHONE + CBConnectPeripheralOptionNotifyOnConnectionKey : @YES, + CBConnectPeripheralOptionNotifyOnNotificationKey : @YES, +#endif + }; +} + +- (NSSet *)characteristicUUIDSetWithPairingUUID:(BOOL)pairing { + NSMutableSet *uuidSet = [NSMutableSet set]; + [uuidSet addObject:_toPeripheralCharacteristic.UUID]; + [uuidSet addObject:_fromPeripheralCharacteristic.UUID]; + if (pairing) { + [uuidSet addObject:_pairingCharacteristic.UUID]; + } + return uuidSet; +} + +- (void)checkCharacteristicsUUID:(NSArray *)uuids withPairingUUID:(BOOL)pairing { + NSSet *uuidSet = [NSSet setWithArray:uuids]; + XCTAssertEqualObjects(uuidSet, [self characteristicUUIDSetWithPairingUUID:pairing]); +} + +- (CBCharacteristic *)generateCharateristicMockWithIdentifierString:(NSString *)identifierString { + CBCharacteristic *characteristic = OCMStrictClassMock([CBCharacteristic class]); + CBUUID *toPeripheralCharUUID = [CBUUID UUIDWithString:identifierString]; + OCMStub([characteristic UUID]).andReturn(toPeripheralCharUUID); + return characteristic; +} + +- (void)transitionToSocketCommunicationStateWithPairingChar:(BOOL)hasPairingChar { + OCMExpect([_centralManagerMock connectPeripheralForPeer:_centralPeerManager + options:[self peripheralConnectionOptions]]); + [_centralPeerManager socketWithPairingCharacteristic:hasPairingChar + completion:^(GNSSocket *newSocket, NSError *error) { + XCTAssertNil(error); + XCTAssertNotNil(newSocket); + XCTAssertNil(_socket); + _socket = newSocket; + }]; + OCMExpect([_peripheralMock discoverServices:@[ _serviceUUID ]]); + _peripheralState = CBPeripheralStateConnected; + [_centralPeerManager bleConnected]; + OCMExpect([_peripheralMock discoverCharacteristics:[OCMArg checkWithBlock:^BOOL(id obj) { + [self checkCharacteristicsUUID:obj withPairingUUID:hasPairingChar]; + return YES; + }] + forService:_serviceMock]); + [_centralPeerManager peripheral:_peripheralMock didDiscoverServices:nil]; + OCMStub([_serviceMock characteristics]).andReturn(_characteristicsWithPairing); + OCMExpect([_peripheralMock setNotifyValue:YES forCharacteristic:_fromPeripheralCharacteristic]); + [_centralPeerManager peripheral:_peripheralMock + didDiscoverCharacteristicsForService:_serviceMock + error:nil]; + GNSWeaveConnectionRequestPacket *connectionRequest = + [[GNSWeaveConnectionRequestPacket alloc] initWithMinVersion:1 + maxVersion:1 + maxPacketSize:0 + data:nil]; + OCMExpect([_peripheralMock writeValue:[connectionRequest serialize] + forCharacteristic:_toPeripheralCharacteristic + type:CBCharacteristicWriteWithResponse]); + [_centralPeerManager peripheral:_peripheralMock + didUpdateNotificationStateForCharacteristic:_fromPeripheralCharacteristic + error:nil]; + XCTAssertNotNil(_socket); + _socket.delegate = _socketDelegateMock; +} + +- (void)simulateConnectedSocketWithPairingChar:(BOOL)hasPairingChar { + [self transitionToSocketCommunicationStateWithPairingChar:hasPairingChar]; + OCMExpect([_socketDelegateMock socketDidConnect:_socket]); + GNSWeaveConnectionConfirmPacket *confirmConnection = + [[GNSWeaveConnectionConfirmPacket alloc] initWithVersion:1 packetSize:100 data:nil]; + OCMExpect([_fromPeripheralCharacteristic value]).andReturn([confirmConnection serialize]); + OCMExpect([_centralPeerManager.testing_connectionConfirmTimer invalidate]); + [_centralPeerManager peripheral:_peripheralMock + didUpdateValueForCharacteristic:_fromPeripheralCharacteristic + error:nil]; + // When the connection confirm packet is received the socket should be invalidated and released. + // It's important to call |ClearTimer| here, since it retains |_centralPeerManager|. + XCTAssertNil(_centralPeerManager.testing_connectionConfirmTimer); + ClearTimer(); +} + +- (void)testCentralPeerManager { + XCTAssertEqualObjects(_centralPeerManager.identifier, _peripheralUUID); + // The dealloc should set the delegate to nil. + OCMExpect([_peripheralMock setDelegate:nil]); +} + +- (void)testGetSocket { + [self simulateConnectedSocketWithPairingChar:NO]; + XCTAssertNotNil(_socket); + // The dealloc should set the delegate to nil. + OCMExpect([_peripheralMock setDelegate:nil]); +} + +- (void)testBLEDisconnectBeforeConnect { + OCMExpect([_centralManagerMock connectPeripheralForPeer:_centralPeerManager + options:[self peripheralConnectionOptions]]); + [_centralPeerManager socketWithPairingCharacteristic:NO + completion:^(GNSSocket *socket, NSError *error) { + XCTAssertNil(socket); + XCTAssertEqual(error.code, GNSErrorNoConnection); + }]; + _peripheralState = CBPeripheralStateDisconnected; + OCMExpect([_peripheralMock setDelegate:nil]); + OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]); + [_centralPeerManager bleDisconnectedWithError:nil]; +} + +- (void)testBLEDisconnectAfterConnect { + NSError *bleDisconnectError = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + OCMExpect([_centralManagerMock connectPeripheralForPeer:_centralPeerManager + options:[self peripheralConnectionOptions]]); + [_centralPeerManager socketWithPairingCharacteristic:NO + completion:^(GNSSocket *socket, NSError *error) { + XCTAssertEqual(error, bleDisconnectError); + XCTAssertNil(socket); + }]; + OCMExpect([_peripheralMock discoverServices:@[ _serviceUUID ]]); + _peripheralState = CBPeripheralStateConnected; + [_centralPeerManager bleConnected]; + _peripheralState = CBPeripheralStateDisconnected; + OCMExpect([_peripheralMock setDelegate:nil]); + OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]); + [_centralPeerManager bleDisconnectedWithError:bleDisconnectError]; +} + +- (void)testDiscoverServiceError { + NSError *discoverServiceError = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + OCMExpect([_centralManagerMock connectPeripheralForPeer:_centralPeerManager + options:[self peripheralConnectionOptions]]); + [_centralPeerManager socketWithPairingCharacteristic:NO + completion:^(GNSSocket *socket, NSError *error) { + XCTAssertEqual(error, discoverServiceError); + XCTAssertNil(socket); + }]; + OCMExpect([_peripheralMock discoverServices:@[ _serviceUUID ]]); + _peripheralState = CBPeripheralStateConnected; + [_centralPeerManager bleConnected]; + OCMExpect([_centralManagerMock cancelPeripheralConnectionForPeer:_centralPeerManager]); + [_centralPeerManager peripheral:_peripheralMock didDiscoverServices:discoverServiceError]; + _peripheralState = CBPeripheralStateDisconnected; + OCMExpect([_peripheralMock setDelegate:nil]); + OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]); + [_centralPeerManager bleDisconnectedWithError:nil]; +} + +- (void)testDiscoverCharacteristicError { + NSError *discoverCharacteristicError = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + OCMExpect([_centralManagerMock connectPeripheralForPeer:_centralPeerManager + options:[self peripheralConnectionOptions]]); + [_centralPeerManager socketWithPairingCharacteristic:NO + completion:^(GNSSocket *socket, NSError *error) { + XCTAssertEqual(error, discoverCharacteristicError); + XCTAssertNil(socket); + }]; + OCMExpect([_peripheralMock discoverServices:@[ _serviceUUID ]]); + _peripheralState = CBPeripheralStateConnected; + [_centralPeerManager bleConnected]; + OCMExpect([_peripheralMock discoverCharacteristics:[OCMArg checkWithBlock:^BOOL(id obj) { + [self checkCharacteristicsUUID:obj withPairingUUID:NO]; + return YES; + }] + forService:_serviceMock]); + [_centralPeerManager peripheral:_peripheralMock didDiscoverServices:nil]; + OCMExpect([_centralManagerMock cancelPeripheralConnectionForPeer:_centralPeerManager]); + OCMStub([_serviceMock characteristics]).andReturn(_characteristics); + [_centralPeerManager peripheral:_peripheralMock + didDiscoverCharacteristicsForService:_serviceMock + error:discoverCharacteristicError]; + _peripheralState = CBPeripheralStateDisconnected; + OCMExpect([_peripheralMock setDelegate:nil]); + OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]); + [_centralPeerManager bleDisconnectedWithError:nil]; +} + +- (void)testSetNotifyValueForCharacteristicError { + NSError *notificationError = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + OCMExpect([_centralManagerMock connectPeripheralForPeer:_centralPeerManager + options:[self peripheralConnectionOptions]]); + [_centralPeerManager socketWithPairingCharacteristic:NO + completion:^(GNSSocket *socket, NSError *error) { + XCTAssertEqual(error, notificationError); + XCTAssertNil(socket); + }]; + OCMExpect([_peripheralMock discoverServices:@[ _serviceUUID ]]); + _peripheralState = CBPeripheralStateConnected; + [_centralPeerManager bleConnected]; + OCMExpect([_peripheralMock discoverCharacteristics:[OCMArg checkWithBlock:^BOOL(id obj) { + [self checkCharacteristicsUUID:obj withPairingUUID:NO]; + return YES; + }] + forService:_serviceMock]); + [_centralPeerManager peripheral:_peripheralMock didDiscoverServices:nil]; + OCMExpect([_centralManagerMock cancelPeripheralConnectionForPeer:_centralPeerManager]); + OCMStub([_serviceMock characteristics]).andReturn(_characteristics); + OCMExpect([_peripheralMock setNotifyValue:YES forCharacteristic:_fromPeripheralCharacteristic]); + [_centralPeerManager peripheral:_peripheralMock + didDiscoverCharacteristicsForService:_serviceMock + error:nil]; + [_centralPeerManager peripheral:_peripheralMock + didUpdateNotificationStateForCharacteristic:_fromPeripheralCharacteristic + error:notificationError]; + _peripheralState = CBPeripheralStateDisconnected; + OCMExpect([_peripheralMock setDelegate:nil]); + OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]); + [_centralPeerManager bleDisconnectedWithError:nil]; +} + +- (void)testTimeOutWaitingForConnectionResponse { + [self transitionToSocketCommunicationStateWithPairingChar:NO]; + OCMExpect([_centralManagerMock cancelPeripheralConnectionForPeer:_centralPeerManager]); + FireTimer(); + _peripheralState = CBPeripheralStateDisconnected; + OCMExpect([_peripheralMock setDelegate:nil]); + OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]); + OCMExpect([_socketDelegateMock socket:_socket + didDisconnectWithError:[OCMArg checkWithBlock:^BOOL(NSError *error) { + return [error.domain isEqualToString:kGNSSocketsErrorDomain] && + error.code == GNSErrorConnectionTimedOut; + }]]); + [_centralPeerManager bleDisconnectedWithError:nil]; + XCTAssertNil(_centralPeerManager.testing_connectionConfirmTimer); +} + +- (void)testBLEDisconnectedWhileWaitingForConnectionResponse { + [self transitionToSocketCommunicationStateWithPairingChar:NO]; + NSError *bleDisconnectError = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + _peripheralState = CBPeripheralStateDisconnected; + OCMExpect([_peripheralMock setDelegate:nil]); + OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]); + OCMExpect([_socketDelegateMock socket:_socket didDisconnectWithError:bleDisconnectError]); + OCMExpect([_centralPeerManager.testing_connectionConfirmTimer invalidate]); + [_centralPeerManager bleDisconnectedWithError:bleDisconnectError]; + XCTAssertNil(_centralPeerManager.testing_connectionConfirmTimer); +} + +- (void)testCancelPendingSocket { + OCMExpect([_centralManagerMock connectPeripheralForPeer:_centralPeerManager + options:[self peripheralConnectionOptions]]); + [_centralPeerManager + socketWithPairingCharacteristic:NO + completion:^(GNSSocket *socket, NSError *error) { + XCTAssertNil(socket); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, kGNSSocketsErrorDomain); + XCTAssertEqual(error.code, GNSErrorCancelPendingSocketRequested); + }]; + OCMExpect([_peripheralMock discoverServices:@[ _serviceUUID ]]); + _peripheralState = CBPeripheralStateConnected; + [_centralPeerManager bleConnected]; + OCMExpect([_peripheralMock discoverCharacteristics:[OCMArg checkWithBlock:^BOOL(id obj) { + [self checkCharacteristicsUUID:obj withPairingUUID:NO]; + return YES; + }] + forService:_serviceMock]); + [_centralPeerManager peripheral:_peripheralMock didDiscoverServices:nil]; + OCMExpect([_centralManagerMock cancelPeripheralConnectionForPeer:_centralPeerManager]); + [_centralPeerManager cancelPendingSocket]; + _peripheralState = CBPeripheralStateDisconnected; + OCMExpect([_peripheralMock setDelegate:nil]); + OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]); + [_centralPeerManager bleDisconnectedWithError:nil]; +} + +- (void)testPairing { + [self simulateConnectedSocketWithPairingChar:YES]; + XCTAssertNotNil(_socket); + OCMExpect([_peripheralMock readValueForCharacteristic:_pairingCharacteristic]); + [_centralPeerManager startBluetoothPairingWithCompletion:^(BOOL pairing, NSError *error) { + XCTAssertTrue(pairing); + XCTAssertNil(error); + }]; + // The dealloc should set the delegate to nil. + OCMExpect([_peripheralMock setDelegate:nil]); +} + +- (void)testDisconnect { + [self simulateConnectedSocketWithPairingChar:NO]; + OCMExpect([_centralManagerMock cancelPeripheralConnectionForPeer:_centralPeerManager]); + [_socket disconnect]; + _peripheralState = CBPeripheralStateDisconnected; + OCMExpect([_peripheralMock setDelegate:nil]); + OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]); + OCMExpect([_socketDelegateMock socket:_socket didDisconnectWithError:nil]); + [_centralPeerManager bleDisconnectedWithError:nil]; +} + +- (void)testDisconnectWithError { + [self simulateConnectedSocketWithPairingChar:NO]; + OCMExpect([_centralManagerMock cancelPeripheralConnectionForPeer:_centralPeerManager]); + [_socket disconnect]; + NSError *errorWhileBLEDisconnecting = + [[NSError alloc] initWithDomain:@"domain" code:-42 userInfo:nil]; + _peripheralState = CBPeripheralStateDisconnected; + OCMExpect([_peripheralMock setDelegate:nil]); + OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]); + OCMExpect([_socketDelegateMock socket:_socket didDisconnectWithError:errorWhileBLEDisconnecting]); + [_centralPeerManager bleDisconnectedWithError:errorWhileBLEDisconnecting]; + OCMVerifyAll((id)_socketDelegateMock); +} + +- (void)testDropSocketWhileConnecting { + [self transitionToSocketCommunicationStateWithPairingChar:NO]; + OCMExpect([_centralManagerMock cancelPeripheralConnectionForPeer:_centralPeerManager]); + OCMExpect([_centralPeerManager.testing_connectionConfirmTimer invalidate]); + OCMExpect([_peripheralMock setDelegate:nil]); + _socket = nil; +} + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Peripheral/GNSPeripheralManagerTest.m b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Peripheral/GNSPeripheralManagerTest.m new file mode 100644 index 00000000000..2ff4769bda4 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Peripheral/GNSPeripheralManagerTest.m @@ -0,0 +1,836 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import <XCTest/XCTest.h> + +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Peripheral/GNSPeripheralManager+Private.h" +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager+Private.h" +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSSocket+Private.h" +#import "third_party/objective_c/ocmock/v3/Source/OCMock/OCMock.h" + +@interface TestGNSPeripheralManager : GNSPeripheralManager +@property(nonatomic, strong) id cbPeripheralManagerMock; +@property(nonatomic, strong) NSDictionary *cbOptions; +@end + +@implementation TestGNSPeripheralManager + +- (CBPeripheralManager *)cbPeripheralManagerWithDelegate:(id<CBPeripheralManagerDelegate>)delegate + queue:(dispatch_queue_t)queue + options:(NSDictionary *)options { + _cbOptions = options; + return _cbPeripheralManagerMock; +} + +@end + +@interface GNSPeripheralManagerTest : XCTestCase { + TestGNSPeripheralManager *_peripheralManager; + NSString *_displayName; + NSString *_restoreIdentifier; + id _cbPeripheralManagerMock; + NSDictionary *_cbAdvertisementData; + CBPeripheralManagerState _cbPeripheralManagerState; + NSMutableArray *_mocksToVerify; + NSMutableDictionary *_cbServiceStatePerPeripheral; +} +@end + +@implementation GNSPeripheralManagerTest + +- (void)setUp { + _mocksToVerify = [NSMutableArray array]; + _cbServiceStatePerPeripheral = [NSMutableDictionary dictionary]; + _displayName = @"DisplayName"; + _restoreIdentifier = @"RestoreIdentifier"; + _cbPeripheralManagerMock = OCMStrictClassMock([CBPeripheralManager class]); + [_mocksToVerify addObject:_cbPeripheralManagerMock]; + OCMStub([_cbPeripheralManagerMock state]).andDo(^(NSInvocation *invocation) { + [invocation setReturnValue:&_cbPeripheralManagerState]; + }); + OCMStub([_cbPeripheralManagerMock isAdvertising]).andDo(^(NSInvocation *invocation) { + BOOL isAdvertising = _cbAdvertisementData != nil; + [invocation setReturnValue:&isAdvertising]; + }); + OCMStub([_cbPeripheralManagerMock stopAdvertising]).andDo(^(NSInvocation *invocation) { + _cbAdvertisementData = nil; + }); + OCMStub([_cbPeripheralManagerMock + startAdvertising:[OCMArg checkWithBlock:^BOOL(id obj) { + _cbAdvertisementData = obj; + [_peripheralManager peripheralManagerDidStartAdvertising:_cbPeripheralManagerMock + error:nil]; + return YES; + }]]); + _peripheralManager = [[TestGNSPeripheralManager alloc] + initWithAdvertisedName:_displayName + restoreIdentifier:_restoreIdentifier + queue:dispatch_get_main_queue()]; + _peripheralManager.cbPeripheralManagerMock = _cbPeripheralManagerMock; + XCTAssertFalse(_peripheralManager.isStarted); +} + +- (void)tearDown { + if (_peripheralManager.isStarted) { + [self stopPeripheralManager]; + } + if (_peripheralManager.cbPeripheralManager) { + NSDictionary *expectedOptions = + @{CBPeripheralManagerOptionRestoreIdentifierKey : _restoreIdentifier}; + XCTAssertEqualObjects(expectedOptions, _peripheralManager.cbOptions); + OCMVerifyAll(_peripheralManager.cbPeripheralManagerMock); + } + for (id mock in _mocksToVerify) { + OCMVerifyAll(mock); + } +} + +#pragma mark - Start/Stop + +- (void)startPeripheralManagerWithPeripheralManagerState:(CBPeripheralManagerState)state + expectedAdvertisementData:(NSDictionary *)expectedAdvertisement { + [_peripheralManager start]; + [self updatePeripheralManagerWithPeripheralManagerState:state]; + [self checkAdvertisementData:expectedAdvertisement]; +} + +- (void)updatePeripheralManagerWithPeripheralManagerState:(CBPeripheralManagerState)state { + _cbPeripheralManagerState = state; + [_peripheralManager peripheralManagerDidUpdateState:_cbPeripheralManagerMock]; + XCTAssertTrue(_peripheralManager.isStarted); +} + +- (void)checkAdvertisementData:(NSDictionary *)expectedAdvertisement { + if (expectedAdvertisement) { + XCTAssertEqualObjects(expectedAdvertisement[CBAdvertisementDataLocalNameKey], + _cbAdvertisementData[CBAdvertisementDataLocalNameKey]); + NSSet *expectedUUIDs = + [NSSet setWithArray:expectedAdvertisement[CBAdvertisementDataServiceUUIDsKey]]; + NSSet *advertisedUUIDs = + [NSSet setWithArray:_cbAdvertisementData[CBAdvertisementDataServiceUUIDsKey]]; + XCTAssertEqualObjects(expectedUUIDs, advertisedUUIDs); + } else { + XCTAssertNil(_cbAdvertisementData); + } +} + +- (void)updatePeripheralManagerWithPeripheralManagerState:(CBPeripheralManagerState)state + expectedAdvertisementData:(NSDictionary *)expectedAdvertisement { + _cbPeripheralManagerState = state; + [_peripheralManager peripheralManagerDidUpdateState:_cbPeripheralManagerMock]; + XCTAssertTrue(_peripheralManager.isStarted); + if (expectedAdvertisement) { + XCTAssertEqualObjects(expectedAdvertisement[CBAdvertisementDataLocalNameKey], + _cbAdvertisementData[CBAdvertisementDataLocalNameKey]); + NSSet *expectedUUIDs = + [NSSet setWithArray:expectedAdvertisement[CBAdvertisementDataServiceUUIDsKey]]; + NSSet *advertisedUUIDs = + [NSSet setWithArray:_cbAdvertisementData[CBAdvertisementDataServiceUUIDsKey]]; + XCTAssertEqualObjects(expectedUUIDs, advertisedUUIDs); + } else { + XCTAssertNil(_cbAdvertisementData); + } +} + +- (void)stopPeripheralManager { + OCMExpect([_cbPeripheralManagerMock removeAllServices]); + OCMExpect([_cbPeripheralManagerMock setDelegate:nil]); + [_peripheralManager stop]; + XCTAssertNil(_cbAdvertisementData); + XCTAssertFalse(_peripheralManager.isStarted); +} + +// Starts with bluetooth off. + +- (void)testStartWithNoServiceBluetoothResetting { + [_peripheralManager start]; + OCMExpect([_peripheralManager.cbPeripheralManager removeAllServices]); + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStateResetting]; + [self checkAdvertisementData:nil]; +} + +- (void)testStartWithNoServiceBluetoothUnknown { + [_peripheralManager start]; + OCMExpect([_peripheralManager.cbPeripheralManager removeAllServices]); + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStateUnknown]; + [self checkAdvertisementData:nil]; +} + +- (void)testStartWithNoServiceBluetoothUnauthorized { + [_peripheralManager start]; + OCMExpect([_peripheralManager.cbPeripheralManager removeAllServices]); + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStateUnauthorized]; + [self checkAdvertisementData:nil]; +} + +- (void)testStartWithNoServiceBluetoothUnsupported { + OCMStub([_cbPeripheralManagerMock isAdvertising]).andReturn(NO); + [self startPeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStateUnsupported + expectedAdvertisementData:nil]; +} + +- (void)testStartWithNoServiceBluetoothOff { + NSDictionary *expectedAdvertisingData = @{ + CBAdvertisementDataServiceUUIDsKey : @[], + CBAdvertisementDataLocalNameKey : _displayName, + }; + [self startPeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOff + expectedAdvertisementData:expectedAdvertisingData]; +} + +// Starts with bluetooth on: should advertise local name on BLE. +- (void)testStartWithNoServiceBluetoothOn { + NSDictionary *expectedAdvertisingData = @{ + CBAdvertisementDataServiceUUIDsKey : @[], + CBAdvertisementDataLocalNameKey : _displayName, + }; + [self startPeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn + expectedAdvertisementData:expectedAdvertisingData]; +} + +// Starts with BLE unknown, and then turn on bluetooth. Advertisement should be done only after +// turning on bluetooth. +- (void)testStartWithBleStateUnknownAndTurnOnBluetooth { + [_peripheralManager start]; + OCMExpect([_peripheralManager.cbPeripheralManager removeAllServices]); + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStateUnknown]; + [self checkAdvertisementData:nil]; + + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn + expectedAdvertisementData:@{ + CBAdvertisementDataServiceUUIDsKey : @[], + CBAdvertisementDataLocalNameKey : _displayName, + }]; +} + +- (void)testStartWithBleStateResettingAndTurnOnBluetooth { + [_peripheralManager start]; + OCMExpect([_peripheralManager.cbPeripheralManager removeAllServices]); + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStateUnknown]; + [self checkAdvertisementData:nil]; + + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn + expectedAdvertisementData:@{ + CBAdvertisementDataServiceUUIDsKey : @[], + CBAdvertisementDataLocalNameKey : _displayName, + }]; +} + +#pragma mark - Restore + +- (void)testRestore { + [_peripheralManager start]; + GNSPeripheralServiceManager *service1 = + [self peripheralServiceManagerWithServiceState:GNSBluetoothServiceStateAdded]; + OCMStub([service1 isAdvertising]).andReturn(YES); + [_peripheralManager addPeripheralServiceManager:service1 bleServiceAddedCompletion:nil]; + GNSPeripheralServiceManager *service2 = + [self peripheralServiceManagerWithServiceState:GNSBluetoothServiceStateAdded]; + OCMStub([service2 isAdvertising]).andReturn(YES); + [_peripheralManager addPeripheralServiceManager:service2 bleServiceAddedCompletion:nil]; + CBMutableService *restoredCBService1 = OCMStrictClassMock([CBMutableService class]); + OCMStub([restoredCBService1 UUID]).andReturn(service1.serviceUUID); + CBMutableService *restoredCBService2 = OCMStrictClassMock([CBMutableService class]); + OCMStub([restoredCBService2 UUID]).andReturn(service2.serviceUUID); + NSDictionary *state = @{ + CBPeripheralManagerRestoredStateAdvertisementDataKey : @"Name", + CBPeripheralManagerRestoredStateServicesKey : @[ restoredCBService1, restoredCBService2 ], + }; + OCMExpect([service1 restoredCBService:restoredCBService1]); + OCMExpect([service2 restoredCBService:restoredCBService2]); + [_peripheralManager peripheralManager:_cbPeripheralManagerMock willRestoreState:state]; + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn]; + [self checkAdvertisementData:@{ + CBAdvertisementDataServiceUUIDsKey : @[ service1.serviceUUID, service2.serviceUUID ], + CBAdvertisementDataLocalNameKey : _displayName, + }]; +} + +#pragma mark - Advertising + +// Generates a peripheral service manager mock ready to be added to |_peripheralManager|. +- (GNSPeripheralServiceManager *)peripheralServiceManager { + return [self peripheralServiceManagerWithServiceState:GNSBluetoothServiceStateNotAdded]; +} + +// Generates a peripheral service manager mock |_peripheralManager| with the cbServiceState +// set to |initialServiceState|. +- (GNSPeripheralServiceManager *)peripheralServiceManagerWithServiceState: + (GNSBluetoothServiceState)initialServiceState { + CBUUID *uuid = [CBUUID UUIDWithNSUUID:[NSUUID UUID]]; + CBMutableService *cbService = OCMClassMock([CBMutableService class]); + OCMStub([cbService UUID]).andReturn(uuid); + [_mocksToVerify addObject:cbService]; + GNSPeripheralServiceManager *peripheralServiceManager = + OCMStrictClassMock([GNSPeripheralServiceManager class]); + [_mocksToVerify addObject:peripheralServiceManager]; + OCMStub([peripheralServiceManager cbService]).andReturn(cbService); + OCMStub([peripheralServiceManager serviceUUID]).andReturn(uuid); + OCMExpect([peripheralServiceManager addedToPeripheralManager:_peripheralManager + bleServiceAddedCompletion:[OCMArg any]]); + + __block GNSBluetoothServiceState cbServiceState = initialServiceState; + OCMStub([peripheralServiceManager cbServiceState]).andDo(^(NSInvocation *invocation) { + GNSBluetoothServiceState stateToReturn = cbServiceState; + return [invocation setReturnValue:&stateToReturn]; + }); + OCMStub([peripheralServiceManager willAddCBService]).andDo(^(NSInvocation *invocation) { + cbServiceState = GNSBluetoothServiceStateAddInProgress; + }); + + OCMStub([peripheralServiceManager didAddCBServiceWithError:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + __unsafe_unretained NSError *error = nil; + [invocation getArgument:&error atIndex:2]; + if (!error) { + cbServiceState = GNSBluetoothServiceStateAdded; + } else { + cbServiceState = GNSBluetoothServiceStateNotAdded; + } + }); + OCMStub([peripheralServiceManager didRemoveCBService]).andDo(^(NSInvocation *invocation) { + cbServiceState = GNSBluetoothServiceStateNotAdded; + }); + return peripheralServiceManager; +} + +// Adds a peripheral service manager mock into |_peripheralManager|. +- (void)addPeripheralManager:(id)peripheralServiceManager { + CBMutableService *service = [peripheralServiceManager cbService]; + OCMExpect([_cbPeripheralManagerMock addService:service]); + [_peripheralManager addPeripheralServiceManager:peripheralServiceManager + bleServiceAddedCompletion:nil]; +} + +// Adds one peripheral service manager. +- (void)testAddOneServiceStartsAdvertisment { + GNSPeripheralServiceManager *peripheralServiceManager = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES); + // Starting before the services are added should not start adverting. + [self addPeripheralManager:peripheralServiceManager]; + [_peripheralManager start]; + XCTAssertEqual(GNSBluetoothServiceStateNotAdded, [peripheralServiceManager cbServiceState]); + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn]; + XCTAssertEqual(GNSBluetoothServiceStateAddInProgress, [peripheralServiceManager cbServiceState]); + [self checkAdvertisementData:nil]; + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + didAddService:[peripheralServiceManager cbService] + error:nil]; + XCTAssertEqual(GNSBluetoothServiceStateAdded, [peripheralServiceManager cbServiceState]); + + // Adding the service should start the advertisment. + [self checkAdvertisementData:@{ + CBAdvertisementDataServiceUUIDsKey : @[ [peripheralServiceManager serviceUUID] ], + CBAdvertisementDataLocalNameKey : _displayName, + }]; +} + +// Adds one peripheral service manager fails with error. +- (void)testAddOneServiceError { + GNSPeripheralServiceManager *peripheralServiceManager = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES); + [self addPeripheralManager:peripheralServiceManager]; + [_peripheralManager start]; + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn]; + NSError *expectedError = [[NSError alloc] initWithDomain:@"Test" code:-42 userInfo:nil]; + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + didAddService:[peripheralServiceManager cbService] + error:expectedError]; + [self checkAdvertisementData:nil]; + XCTAssertEqual(GNSBluetoothServiceStateNotAdded, [peripheralServiceManager cbServiceState]); +} + +// Adds two peripheral service managers, one that advertise itself, and one that doesn't. Only +// one service should be advertised. +- (void)testAddTwoServices { + GNSPeripheralServiceManager *peripheralServiceManager1 = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager1 isAdvertising]).andReturn(NO); + [self addPeripheralManager:peripheralServiceManager1]; + GNSPeripheralServiceManager *peripheralServiceManager2 = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager2 isAdvertising]).andReturn(YES); + [self addPeripheralManager:peripheralServiceManager2]; + [_peripheralManager start]; + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn]; + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + didAddService:[peripheralServiceManager1 cbService] + error:nil]; + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + didAddService:[peripheralServiceManager2 cbService] + error:nil]; + NSDictionary *expectedAdvertisement = @{ + CBAdvertisementDataServiceUUIDsKey : @[ [peripheralServiceManager2 serviceUUID] ], + CBAdvertisementDataLocalNameKey : _displayName, + }; + [self checkAdvertisementData:expectedAdvertisement]; +} + +// Adds one peripheral service manager while not advertising. Changes the advertising value for this +// peripheral service manager. +- (void)testAddServiceAndChangeAdvertisingValue { + GNSPeripheralServiceManager *peripheralServiceManager = [self peripheralServiceManager]; + OCMStubRecorder *isAdvertisingStub = OCMStub([peripheralServiceManager isAdvertising]); + isAdvertisingStub.andReturn(NO); + [self addPeripheralManager:peripheralServiceManager]; + [_peripheralManager start]; + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn]; + [self checkAdvertisementData:nil]; + XCTAssertEqual(GNSBluetoothServiceStateAddInProgress, [peripheralServiceManager cbServiceState]); + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + didAddService:[peripheralServiceManager cbService] + error:nil]; + [self checkAdvertisementData:@{ + CBAdvertisementDataServiceUUIDsKey : @[], + CBAdvertisementDataLocalNameKey : _displayName, + }]; + isAdvertisingStub.andReturn(YES); + [_peripheralManager updateAdvertisedServices]; + [self checkAdvertisementData:@{ + CBAdvertisementDataServiceUUIDsKey : @[ [peripheralServiceManager serviceUUID] ], + CBAdvertisementDataLocalNameKey : _displayName, + }]; + return; +} + +- (void)startWithPeripheralServiceManagers:(NSArray *)managers { + NSMutableArray *advertisedServiceUUIDs = [NSMutableArray array]; + for (GNSPeripheralServiceManager *peripheralServiceManager in managers) { + [self addPeripheralManager:peripheralServiceManager]; + if (peripheralServiceManager.isAdvertising) { + [advertisedServiceUUIDs addObject:peripheralServiceManager.serviceUUID]; + } + } + [_peripheralManager start]; + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn]; + for (GNSPeripheralServiceManager *peripheralServiceManager in managers) { + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + didAddService:[peripheralServiceManager cbService] + error:nil]; + } + [self checkAdvertisementData:@{ + CBAdvertisementDataServiceUUIDsKey : advertisedServiceUUIDs, + CBAdvertisementDataLocalNameKey : _displayName, + }]; +} + +#pragma mark - Subscribe/unsubscribe characteristics + +// Creates a characteristic mock, and adds it into |_mocksToVerify|. If no service is provided, +// a service mock is created (added into |_mocksToVerify| too). The service uuid is attach +// to the service. If no service uuid is provided, an uuid is created. +- (CBMutableCharacteristic *)prepareCharacteristicForCBService:(CBService *)cbService + withServiceUUID:(CBUUID *)serviceUUID { + if (!serviceUUID) { + serviceUUID = [CBUUID UUIDWithNSUUID:[NSUUID UUID]]; + } + if (!cbService) { + cbService = OCMClassMock([CBService class]); + OCMStub([cbService UUID]).andReturn(serviceUUID); + [_mocksToVerify addObject:cbService]; + } + CBMutableCharacteristic *characteristicMock = OCMClassMock([CBMutableCharacteristic class]); + OCMStub([characteristicMock service]).andReturn(cbService); + [_mocksToVerify addObject:characteristicMock]; + return characteristicMock; +} + +// Creates a characteristic mock based on the service from the peripheral service manager. +// The mock is added into _mocksToVerify. +- (CBMutableCharacteristic *)prepareCharacteristicForPeripheralServiceManager: + (GNSPeripheralServiceManager *)peripheralServiceManager { + CBService *cbService = peripheralServiceManager.cbService; + CBUUID *serviceUUID = cbService.UUID; + return [self prepareCharacteristicForCBService:cbService withServiceUUID:serviceUUID]; +} + +// Adds a peripheral service manager, and a central subscribes to its service. +- (void)testCentralDidSubscribe { + id peripheralServiceManager = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES); + [self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]]; + id centralMock = OCMClassMock([CBCentral class]); + CBMutableCharacteristic *characteristicMock = + [self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager]; + OCMExpect([peripheralServiceManager central:centralMock + didSubscribeToCharacteristic:characteristicMock]); + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + central:centralMock + didSubscribeToCharacteristic:characteristicMock]; + OCMVerifyAll(centralMock); +} + +// Adds a peripheral service manager, and a central unsubscribes to its service. +- (void)testCentralDidUnsubscribe { + id peripheralServiceManager = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES); + [self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]]; + id centralMock = OCMClassMock([CBCentral class]); + CBMutableCharacteristic *characteristicMock = + [self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager]; + OCMExpect([peripheralServiceManager central:centralMock + didUnsubscribeFromCharacteristic:characteristicMock]); + + // As a workaourd for b/31752176 We are restarting the peripheral manager after the central + // unsubscribe. + OCMExpect([_cbPeripheralManagerMock removeAllServices]); + OCMExpect([_cbPeripheralManagerMock setDelegate:nil]); + + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + central:centralMock + didUnsubscribeFromCharacteristic:characteristicMock]; + OCMVerifyAll(centralMock); +} + +#pragma mark - Characteristic requests + +// Creates an ATT request mock based on the characteristic. The mock is added into |_mocksToVerify| +- (CBATTRequest *)prepareRequestForCharacteristic:(CBMutableCharacteristic *)characteristic { + id requestMock = OCMClassMock([CBATTRequest class]); + OCMStub([requestMock characteristic]).andReturn(characteristic); + [_mocksToVerify addObject:requestMock]; + return requestMock; +} + +// Adds a peripheral service manager, and receives a read request accepted by the manager. The +// read request should be processed. +- (void)testDidReceiveReadRequest { + id peripheralServiceManager = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES); + [self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]]; + CBMutableCharacteristic *characteristicMock = + [self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager]; + CBATTRequest *readRequestMock = [self prepareRequestForCharacteristic:characteristicMock]; + OCMStub([peripheralServiceManager canProcessReadRequest:readRequestMock]) + .andReturn(CBATTErrorSuccess); + OCMExpect([peripheralServiceManager processReadRequest:readRequestMock]); + OCMExpect( + [_cbPeripheralManagerMock respondToRequest:readRequestMock withResult:CBATTErrorSuccess]); + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + didReceiveReadRequest:readRequestMock]; +} + +// Adds a peripheral service manager, and receives a read request refused by the manager. The +// read request should not be processed. +- (void)testDidReceiveBadReadRequest { + id peripheralServiceManager = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES); + [self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]]; + CBMutableCharacteristic *characteristicMock = + [self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager]; + CBATTRequest *readRequestMock = [self prepareRequestForCharacteristic:characteristicMock]; + OCMStub([peripheralServiceManager canProcessReadRequest:readRequestMock]) + .andReturn(CBATTErrorReadNotPermitted); + OCMExpect([_cbPeripheralManagerMock respondToRequest:readRequestMock + withResult:CBATTErrorReadNotPermitted]); + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + didReceiveReadRequest:readRequestMock]; +} + +// Adds a peripheral service manager, and receives a read request to another service. The read +// request should not be processed. +- (void)testDidReceiveReadRequestToWrongService { + id peripheralServiceManager = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES); + [self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]]; + CBMutableCharacteristic *characteristicMock = + [self prepareCharacteristicForCBService:nil withServiceUUID:nil]; + CBATTRequest *readRequestMock = [self prepareRequestForCharacteristic:characteristicMock]; + OCMExpect([_cbPeripheralManagerMock respondToRequest:readRequestMock + withResult:CBATTErrorAttributeNotFound]); + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + didReceiveReadRequest:readRequestMock]; +} + +// Adds a peripheral service manager, and receives a write request accepted by the manager. The +// write request should be processed. +- (void)testDidReceiveOneWriteRequest { + id peripheralServiceManager = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES); + [self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]]; + CBMutableCharacteristic *characteristicMock = + [self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager]; + CBATTRequest *writeRequestMock = [self prepareRequestForCharacteristic:characteristicMock]; + OCMStub([peripheralServiceManager canProcessWriteRequest:writeRequestMock]) + .andReturn(CBATTErrorSuccess); + OCMExpect([peripheralServiceManager processWriteRequest:writeRequestMock]); + OCMExpect( + [_cbPeripheralManagerMock respondToRequest:writeRequestMock withResult:CBATTErrorSuccess]); + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + didReceiveWriteRequests:@[ writeRequestMock ]]; +} + +// Adds a peripheral service manager, and receives a write request refused by the manager. The write +// request should not be processed. +- (void)testDidReceiveOneBadWriteRequest { + id peripheralServiceManager = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES); + [self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]]; + CBMutableCharacteristic *characteristicMock = + [self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager]; + CBATTRequest *writeRequestMock = [self prepareRequestForCharacteristic:characteristicMock]; + OCMStub([peripheralServiceManager canProcessWriteRequest:writeRequestMock]) + .andReturn(CBATTErrorWriteNotPermitted); + OCMExpect([_cbPeripheralManagerMock respondToRequest:writeRequestMock + withResult:CBATTErrorWriteNotPermitted]); + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + didReceiveWriteRequests:@[ writeRequestMock ]]; +} + +// Adds a peripheral service manager, and receives a write request to another service. The write +// request should not be processed. +- (void)testDidReceiveOneWriteRequestToWrongService { + id peripheralServiceManager = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES); + [self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]]; + CBMutableCharacteristic *characteristicMock = + [self prepareCharacteristicForCBService:nil withServiceUUID:nil]; + CBATTRequest *writeRequestMock = [self prepareRequestForCharacteristic:characteristicMock]; + OCMStub([_cbPeripheralManagerMock respondToRequest:writeRequestMock + withResult:CBATTErrorAttributeNotFound]); + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + didReceiveWriteRequests:@[ writeRequestMock ]]; +} + +// Adds a peripheral service manager, and receives two write requests, only the second one is +// refused by the manager. None of those write requests should be processed. The error should be +// sent to the first write request. +- (void)testDidReceiveWriteRequestOneGoodAndOneBad { + id peripheralServiceManager1 = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager1 isAdvertising]).andReturn(YES); + id peripheralServiceManager2 = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager2 isAdvertising]).andReturn(YES); + [self + startWithPeripheralServiceManagers:@[ peripheralServiceManager1, peripheralServiceManager2 ]]; + // Good request + CBMutableCharacteristic *characteristic1Mock = + [self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager1]; + CBATTRequest *writeRequest1Mock = [self prepareRequestForCharacteristic:characteristic1Mock]; + OCMStub([peripheralServiceManager1 canProcessWriteRequest:writeRequest1Mock]) + .andReturn(CBATTErrorSuccess); + // Bad request + CBMutableCharacteristic *characteristic2Mock = + [self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager2]; + CBATTRequest *writeRequest2Mock = [self prepareRequestForCharacteristic:characteristic2Mock]; + OCMStub([peripheralServiceManager2 canProcessWriteRequest:writeRequest2Mock]) + .andReturn(CBATTErrorWriteNotPermitted); + OCMExpect([_cbPeripheralManagerMock respondToRequest:writeRequest1Mock + withResult:CBATTErrorWriteNotPermitted]); + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + didReceiveWriteRequests:@[ writeRequest1Mock, writeRequest2Mock ]]; +} + +// Adds a peripheral service manager, and receives two write requests, only the first one is +// refused by the manager. None of those write requests should be processed. The error should be +// sent to the first write request. +- (void)testDidReceiveWriteRequestOneBadAndOneGood { + id peripheralServiceManager1 = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager1 isAdvertising]).andReturn(YES); + id peripheralServiceManager2 = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager2 isAdvertising]).andReturn(YES); + [self + startWithPeripheralServiceManagers:@[ peripheralServiceManager1, peripheralServiceManager2 ]]; + // Good request + CBMutableCharacteristic *characteristic1Mock = + [self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager1]; + CBATTRequest *writeRequest1Mock = [self prepareRequestForCharacteristic:characteristic1Mock]; + OCMStub([peripheralServiceManager1 canProcessWriteRequest:writeRequest1Mock]) + .andReturn(CBATTErrorWriteNotPermitted); + // Bad request + CBMutableCharacteristic *characteristic2Mock = + [self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager2]; + CBATTRequest *writeRequest2Mock = [self prepareRequestForCharacteristic:characteristic2Mock]; + OCMStub([peripheralServiceManager2 canProcessWriteRequest:writeRequest2Mock]) + .andReturn(CBATTErrorSuccess); + OCMExpect([_cbPeripheralManagerMock respondToRequest:writeRequest1Mock + withResult:CBATTErrorWriteNotPermitted]); + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + didReceiveWriteRequests:@[ writeRequest1Mock, writeRequest2Mock ]]; +} + +// Adds a peripheral service manager, and receives two good write requests. Both write request +// should be processed. The success should be sent to the first write request. +- (void)testDidReceiveWriteRequestTwoGood { + id peripheralServiceManager1 = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager1 isAdvertising]).andReturn(YES); + id peripheralServiceManager2 = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager2 isAdvertising]).andReturn(YES); + [self + startWithPeripheralServiceManagers:@[ peripheralServiceManager1, peripheralServiceManager2 ]]; + // Good request + CBMutableCharacteristic *characteristic1Mock = + [self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager1]; + CBATTRequest *writeRequest1Mock = [self prepareRequestForCharacteristic:characteristic1Mock]; + OCMStub([peripheralServiceManager1 canProcessWriteRequest:writeRequest1Mock]) + .andReturn(CBATTErrorSuccess); + OCMExpect([peripheralServiceManager1 processWriteRequest:writeRequest1Mock]); + // Good request + CBMutableCharacteristic *characteristic2Mock = + [self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager2]; + CBATTRequest *writeRequest2Mock = [self prepareRequestForCharacteristic:characteristic2Mock]; + OCMStub([peripheralServiceManager2 canProcessWriteRequest:writeRequest2Mock]) + .andReturn(CBATTErrorSuccess); + OCMExpect([peripheralServiceManager2 processWriteRequest:writeRequest2Mock]); + OCMExpect( + [_cbPeripheralManagerMock respondToRequest:writeRequest1Mock withResult:CBATTErrorSuccess]); + [_peripheralManager peripheralManager:_cbPeripheralManagerMock + didReceiveWriteRequests:@[ writeRequest1Mock, writeRequest2Mock ]]; +} + +- (GNSSocket *)createSocketMock { + GNSSocket *socketMock = OCMStrictClassMock([GNSSocket class]); + NSUUID *socketIdentifier = [NSUUID UUID]; + OCMStub([socketMock socketIdentifier]).andReturn(socketIdentifier); + [_mocksToVerify addObject:socketMock]; + return socketMock; +} + +// Adds one update value handler. Should be called right now. +- (void)testIsReadyToUpdateSubscribers { + __block BOOL updateValueHandlerCalled = NO; + GNSSocket *socketMock = [self createSocketMock]; + // Should be called right now. + [_peripheralManager updateOutgoingCharOnSocket:socketMock + withHandler:^() { + updateValueHandlerCalled = YES; + return GNSOutgoingCharUpdateNoReschedule; + }]; + XCTAssertTrue(updateValueHandlerCalled); +} + +// Adds one update value handler. The first one fails. Adds a second update value. It should not +// be called. The first one is set to not fail anymore. When the CB peripheral manager is ready +// to update subscribers, both handlers should be called. +- (void)testIsReadyToUpdateSubscriberBlocked { + __block BOOL updateValueHandler1Called = NO; + __block GNSOutgoingCharUpdate updateValueHandler1ReturnedValue = + GNSOutgoingCharUpdateScheduleLater; + GNSSocket *socketMock = [self createSocketMock]; + // Should be called right now. + [_peripheralManager updateOutgoingCharOnSocket:socketMock + withHandler:^() { + updateValueHandler1Called = YES; + return updateValueHandler1ReturnedValue; + }]; + XCTAssertTrue(updateValueHandler1Called); + __block BOOL updateValueHandler2Called = NO; + __block GNSOutgoingCharUpdate updateValueHandler2ReturnedValue = + GNSOutgoingCharUpdateNoReschedule; + // Should no be called since the previous update value handler failed. + [_peripheralManager updateOutgoingCharOnSocket:socketMock + withHandler:^() { + updateValueHandler2Called = YES; + return updateValueHandler2ReturnedValue; + }]; + XCTAssertFalse(updateValueHandler2Called); + updateValueHandler1ReturnedValue = GNSOutgoingCharUpdateNoReschedule; + updateValueHandler1Called = NO; + // Should again again the first handler, and the second right after. + [_peripheralManager peripheralManagerIsReadyToUpdateSubscribers:_cbPeripheralManagerMock]; + XCTAssertTrue(updateValueHandler1Called); + XCTAssertTrue(updateValueHandler2Called); +} + +// Adds 2 handlers on different sockets. One fails, the second one should process right away. +- (void)testTwoUpdateBlocksOnDifferentSocket { + __block BOOL updateValueHandler1Called = NO; + __block GNSOutgoingCharUpdate updateValueHandler1ReturnedValue = + GNSOutgoingCharUpdateScheduleLater; + GNSSocket *socketMock1 = [self createSocketMock]; + // Should be called right now. + [_peripheralManager updateOutgoingCharOnSocket:socketMock1 + withHandler:^() { + updateValueHandler1Called = YES; + return updateValueHandler1ReturnedValue; + }]; + XCTAssertTrue(updateValueHandler1Called); + __block BOOL updateValueHandler2Called = NO; + __block GNSOutgoingCharUpdate updateValueHandler2ReturnedValue = + GNSOutgoingCharUpdateNoReschedule; + GNSSocket *socketMock2 = [self createSocketMock]; + // Should no be called since the previous update value handler failed. + [_peripheralManager updateOutgoingCharOnSocket:socketMock2 + withHandler:^() { + updateValueHandler2Called = YES; + return updateValueHandler2ReturnedValue; + }]; + XCTAssertTrue(updateValueHandler2Called); + updateValueHandler2Called = NO; + updateValueHandler1ReturnedValue = GNSOutgoingCharUpdateNoReschedule; + updateValueHandler1Called = NO; + // Should again again the first handler, and the second one should not be called. + [_peripheralManager peripheralManagerIsReadyToUpdateSubscribers:_cbPeripheralManagerMock]; + XCTAssertTrue(updateValueHandler1Called); + XCTAssertFalse(updateValueHandler2Called); +} + +- (void)testUpdateBlockCleanupAfterDisconnect { + __block BOOL updateValueHandlerCalled = NO; + GNSSocket *socketMock = [self createSocketMock]; + __block GNSOutgoingCharUpdate outgoingCharUpdateValue = GNSOutgoingCharUpdateScheduleLater; + // Should be called right now. + [_peripheralManager updateOutgoingCharOnSocket:socketMock + withHandler:^() { + updateValueHandlerCalled = YES; + return outgoingCharUpdateValue; + }]; + XCTAssertTrue(updateValueHandlerCalled); + updateValueHandlerCalled = NO; + outgoingCharUpdateValue = GNSOutgoingCharUpdateNoReschedule; + OCMStub([socketMock isConnected]).andReturn(NO); + [_peripheralManager socketDidDisconnect:socketMock]; + // Should again again the first handler, and the second right after. + [_peripheralManager peripheralManagerIsReadyToUpdateSubscribers:_cbPeripheralManagerMock]; + XCTAssertTrue(updateValueHandlerCalled); +} + +- (void)testUpdateValueForCentral { + NSData *data = [NSData data]; + id characteristicMock = OCMStrictClassMock([CBMutableCharacteristic class]); + id centralMock = OCMStrictClassMock([CBCentral class]); + OCMStub([_cbPeripheralManagerMock updateValue:data + forCharacteristic:characteristicMock + onSubscribedCentrals:@[ centralMock ]]); + GNSPeripheralServiceManager *peripheralServiceManager = + OCMStrictClassMock([GNSPeripheralServiceManager class]); + OCMStub([peripheralServiceManager weaveOutgoingCharacteristic]).andReturn(characteristicMock); + GNSSocket *socketMock = OCMStrictClassMock([GNSSocket class]); + OCMStub([socketMock owner]).andReturn(peripheralServiceManager); + OCMStub([socketMock peerAsCentral]).andReturn(centralMock); + [_peripheralManager updateOutgoingCharacteristic:data onSocket:socketMock]; + OCMVerifyAll(characteristicMock); + OCMVerifyAll(centralMock); +} + +- (void)testRecoverFromBTCrashLoop { + GNSPeripheralServiceManager *peripheralServiceManager = [self peripheralServiceManager]; + OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES); + [self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]]; + for (int i = 0; i < 5; ++i) { + OCMExpect([_cbPeripheralManagerMock removeAllServices]); + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStateResetting]; + CBMutableService *service = [peripheralServiceManager cbService]; + OCMExpect([_cbPeripheralManagerMock addService:service]); + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOff]; + XCTAssertEqual(GNSBluetoothServiceStateAddInProgress, + [peripheralServiceManager cbServiceState]); + } + + // After 5 consecutive resetting state, the services are no longer added by the peripheral + // manager. + OCMExpect([_cbPeripheralManagerMock removeAllServices]); + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStateResetting]; + [self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOff]; + XCTAssertEqual(GNSBluetoothServiceStateNotAdded, [peripheralServiceManager cbServiceState]); +} + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Peripheral/GNSPeripheralServiceManagerTest.m b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Peripheral/GNSPeripheralServiceManagerTest.m new file mode 100644 index 00000000000..47aa3269e46 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Peripheral/GNSPeripheralServiceManagerTest.m @@ -0,0 +1,671 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import <XCTest/XCTest.h> + +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Peripheral/GNSPeripheralManager+Private.h" +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager+Private.h" +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSSocket+Private.h" +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSUtils.h" +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSWeavePacket.h" +#import "third_party/objective_c/ocmock/v3/Source/OCMock/OCMock.h" + +@interface GNSPeripheralServiceManagerTest : XCTestCase { + GNSPeripheralServiceManager *_peripheralServiceManager; + CBUUID *_serviceUUID; + BOOL _shouldAcceptSocket; + GNSSocket *_receivedSocket; + id _peripheralManagerMock; + id _cbPeripheralManagerMock; + id _socketDelegateMock; + NSMutableArray *_mocksToVerify; + NSUInteger _centralMaximumUpdateValueLength; + NSUInteger _packetSize; +} +@end + +@implementation GNSPeripheralServiceManagerTest + +- (void)setUp { + _mocksToVerify = [NSMutableArray array]; + _serviceUUID = [CBUUID UUIDWithString:@"3C672799-2B3F-4D93-9E57-29D5C5B01092"];; + _peripheralManagerMock = OCMStrictClassMock([GNSPeripheralManager class]); + _cbPeripheralManagerMock = OCMStrictClassMock([CBPeripheralManager class]); + _socketDelegateMock = OCMStrictProtocolMock(@protocol(GNSSocketDelegate)); + _centralMaximumUpdateValueLength = 100; + _packetSize = 100; + OCMStub([_peripheralManagerMock cbPeripheralManager]).andReturn(_cbPeripheralManagerMock); + _peripheralServiceManager = [[GNSPeripheralServiceManager alloc] + initWithBleServiceUUID:_serviceUUID + addPairingCharacteristic:NO + shouldAcceptSocketHandler:^BOOL(GNSSocket *socket) { + _receivedSocket = socket; + socket.delegate = _socketDelegateMock; + return self->_shouldAcceptSocket; + }]; + XCTAssertEqualObjects(_peripheralServiceManager.serviceUUID, _serviceUUID); + [_peripheralServiceManager addedToPeripheralManager:_peripheralManagerMock + bleServiceAddedCompletion:nil]; + XCTAssertEqual(GNSBluetoothServiceStateNotAdded, _peripheralServiceManager.cbServiceState); + XCTAssertNil(_peripheralServiceManager.cbService); + [_peripheralServiceManager willAddCBService]; + XCTAssertNotNil(_peripheralServiceManager.cbService); + XCTAssertEqual(GNSBluetoothServiceStateAddInProgress, _peripheralServiceManager.cbServiceState); + [_peripheralServiceManager didAddCBServiceWithError:nil]; + XCTAssertEqual(GNSBluetoothServiceStateAdded, _peripheralServiceManager.cbServiceState); +} + +- (void)tearDown { + OCMVerifyAll(_peripheralManagerMock); + OCMVerifyAll(_cbPeripheralManagerMock); + OCMVerifyAll(_socketDelegateMock); + for (id mock in _mocksToVerify) { + OCMVerifyAll(mock); + } +} + +- (void)testServiceManagerAdded { + GNSPeripheralServiceManager *peripheralServiceManager = [[GNSPeripheralServiceManager alloc] + initWithBleServiceUUID:_serviceUUID + addPairingCharacteristic:NO + shouldAcceptSocketHandler:^BOOL(GNSSocket *socket) { + _receivedSocket = socket; + return _shouldAcceptSocket; + }]; + __block BOOL completionCalled = NO; + [peripheralServiceManager addedToPeripheralManager:_peripheralManagerMock + bleServiceAddedCompletion:^(NSError *error) { + completionCalled = YES; + }]; + XCTAssertEqual(peripheralServiceManager.peripheralManager, _peripheralManagerMock); + XCTAssertFalse(completionCalled); + [peripheralServiceManager willAddCBService]; + XCTAssertFalse(completionCalled); + [peripheralServiceManager didAddCBServiceWithError:nil]; + XCTAssertTrue(completionCalled); +} + +- (void)testPeripheralServiceManagerRestored { + GNSPeripheralServiceManager *manager = + [[GNSPeripheralServiceManager alloc] initWithBleServiceUUID:_serviceUUID + addPairingCharacteristic:NO + shouldAcceptSocketHandler:^BOOL(GNSSocket *socket) { + return NO; + }]; + XCTAssertEqual(GNSBluetoothServiceStateNotAdded, manager.cbServiceState); + + CBMutableCharacteristic *weaveOutgoingChar = OCMStrictClassMock([CBMutableCharacteristic class]); + CBUUID *weaveOutgoingCharUUID = [CBUUID UUIDWithString:@"00000100-0004-1000-8000-001A11000102"]; + OCMStub([weaveOutgoingChar UUID]).andReturn(weaveOutgoingCharUUID); + CBMutableCharacteristic *weaveIncomingChar = OCMStrictClassMock([CBMutableCharacteristic class]); + CBUUID *weaveIncomingCharUUID = [CBUUID UUIDWithString:@"00000100-0004-1000-8000-001A11000101"]; + OCMStub([weaveIncomingChar UUID]).andReturn(weaveIncomingCharUUID); + + CBMutableService *cbService = OCMStrictClassMock([CBMutableService class]); + OCMStub([cbService UUID]).andReturn(_serviceUUID); + NSArray *restoredCharacteristics = @[ weaveOutgoingChar, weaveIncomingChar ]; + OCMStub([cbService characteristics]).andReturn(restoredCharacteristics); + [manager restoredCBService:cbService]; + XCTAssertEqual(GNSBluetoothServiceStateAdded, manager.cbServiceState); + XCTAssertEqual(manager.cbService, cbService); + XCTAssertEqual(manager.weaveOutgoingCharacteristic, weaveOutgoingChar); + XCTAssertEqual(manager.weaveIncomingCharacteristic, weaveIncomingChar); +} + +- (void)testDefaultAdvertisingValue { + XCTAssertTrue(_peripheralServiceManager.isAdvertising); +} + +- (void)testSetAdvertisingValueToTrue { + // Nothing should happen since it is already to YES + _peripheralServiceManager.advertising = YES; +} + +- (void)testSetAdvertisingValueToFalse { + OCMExpect([_peripheralManagerMock updateAdvertisedServices]); + _peripheralServiceManager.advertising = NO; +} + +- (void)testCanProcessReadRequest { + CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]); + OCMStubRecorder *recorder = OCMStub([request characteristic]); + + recorder.andReturn(_peripheralServiceManager.weaveOutgoingCharacteristic); + XCTAssertEqual([_peripheralServiceManager canProcessReadRequest:request], + CBATTErrorReadNotPermitted); + recorder.andReturn(_peripheralServiceManager.weaveIncomingCharacteristic); + XCTAssertEqual([_peripheralServiceManager canProcessReadRequest:request], + CBATTErrorReadNotPermitted); + + OCMVerifyAll((id)request); +} + +- (void)testCanProcessReadRequestOnWrongCharacteristic { + CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]); + CBUUID *uuid = [CBUUID UUIDWithNSUUID:[NSUUID UUID]]; + id characteristicMock = OCMStrictClassMock([CBMutableCharacteristic class]); + OCMStub([characteristicMock UUID]).andReturn(uuid); + OCMStub([request characteristic]).andReturn(characteristicMock); + XCTAssertEqual([_peripheralServiceManager canProcessReadRequest:request], + CBATTErrorAttributeNotFound); + OCMVerifyAll((id)request); + OCMVerifyAll(characteristicMock); +} + +- (void)testCanProcessWriteRequestOnOutgoingCharacteristic { + CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]); + OCMStubRecorder *recorder = OCMStub([request characteristic]); + + recorder.andReturn(_peripheralServiceManager.weaveOutgoingCharacteristic); + XCTAssertEqual([_peripheralServiceManager canProcessWriteRequest:request], + CBATTErrorWriteNotPermitted); + OCMVerifyAll((id)request); +} + +- (void)testCanProcessWriteRequestOnIncomingCharacteristic { + CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]); + OCMStubRecorder *recorder = OCMStub([request characteristic]); + + recorder.andReturn(_peripheralServiceManager.weaveIncomingCharacteristic); + XCTAssertEqual([_peripheralServiceManager canProcessWriteRequest:request], CBATTErrorSuccess); + OCMVerifyAll((id)request); +} + +- (void)testCanProcessWriteRequestOnWrongCharacteristic { + CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]); + CBUUID *uuid = [CBUUID UUIDWithNSUUID:[NSUUID UUID]]; + id characteristicMock = OCMStrictClassMock([CBMutableCharacteristic class]); + OCMStub([characteristicMock UUID]).andReturn(uuid); + OCMStub([request characteristic]).andReturn(characteristicMock); + XCTAssertEqual([_peripheralServiceManager canProcessWriteRequest:request], + CBATTErrorAttributeNotFound); + OCMVerifyAll((id)request); + OCMVerifyAll(characteristicMock); +} + +- (CBCentral *)setupRequest:(id)request + withCharacteristic:(CBMutableCharacteristic *)characteristic + central:(CBCentral *)central { + if (!central) { + NSUUID *identifier = [NSUUID UUID]; + central = OCMStrictClassMock([CBCentral class]); + OCMStub([central identifier]).andReturn(identifier); + OCMStub([central maximumUpdateValueLength]).andReturn(_centralMaximumUpdateValueLength); + [_mocksToVerify addObject:central]; + } + OCMStub([request characteristic]).andReturn(characteristic); + OCMStub([request central]).andReturn(central); + return central; +} + +- (void)testProcessEmptyData { + NSMutableData *data = [NSMutableData data]; + CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]); + OCMStub([request value]).andReturn(data); + + [self setupRequest:request + withCharacteristic:_peripheralServiceManager.weaveIncomingCharacteristic + central:nil]; + [_peripheralServiceManager processWriteRequest:request]; + // should not crash, should do nothing. + OCMVerifyAll((id)request); +} + +#pragma mark - Characteristics + +- (void)testPairingChar { + GNSPeripheralServiceManager *manager = + [[GNSPeripheralServiceManager alloc] initWithBleServiceUUID:_serviceUUID + addPairingCharacteristic:YES + shouldAcceptSocketHandler:^BOOL(GNSSocket *socket) { + return NO; + }]; + [manager willAddCBService]; + [manager didAddCBServiceWithError:nil]; + CBMutableCharacteristic *pairingCharacteristic = manager.pairingCharacteristic; + XCTAssertNotNil(pairingCharacteristic); + XCTAssertEqualObjects(pairingCharacteristic.UUID.UUIDString, + @"17836FBD-8C6A-4B81-83CE-8560629E834B", + @"Wrong pairing characteristic UUID."); + XCTAssertEqual(pairingCharacteristic.properties, CBCharacteristicPropertyRead, + @"Wrong property for pairing characteristic."); + XCTAssertEqual(pairingCharacteristic.permissions, CBAttributePermissionsReadEncryptionRequired, + @"Wrong permission for pairing characteristic."); +} + +- (void)testNoPairingChar { + GNSPeripheralServiceManager *manager = + [[GNSPeripheralServiceManager alloc] initWithBleServiceUUID:_serviceUUID + addPairingCharacteristic:NO + shouldAcceptSocketHandler:^BOOL(GNSSocket *socket) { + return NO; + }]; + [manager willAddCBService]; + [manager didAddCBServiceWithError:nil]; + XCTAssertNil(manager.pairingCharacteristic); +} + +- (void)testOutgoingChar { + CBMutableCharacteristic *weaveOutgoingCharacteristic = + _peripheralServiceManager.weaveOutgoingCharacteristic; + XCTAssertNotNil(weaveOutgoingCharacteristic); + XCTAssertEqualObjects(weaveOutgoingCharacteristic.UUID.UUIDString, + @"00000100-0004-1000-8000-001A11000102", + @"Wrong weave outgoing characteristic UUID."); + XCTAssertEqual(weaveOutgoingCharacteristic.properties, CBCharacteristicPropertyIndicate, + @"Wrong property for weave outgoing characteristic."); + XCTAssertEqual(weaveOutgoingCharacteristic.permissions, CBAttributePermissionsReadable, + @"Wrong permission for weave outgoing characteristic."); +} + +- (void)testIncomingChar { + CBMutableCharacteristic *weaveIncomingCharacteristic = + _peripheralServiceManager.weaveIncomingCharacteristic; + XCTAssertNotNil(weaveIncomingCharacteristic); + XCTAssertEqualObjects(weaveIncomingCharacteristic.UUID.UUIDString, + @"00000100-0004-1000-8000-001A11000101", + @"Wrong weave incoming characteristic UUID."); + XCTAssertEqual(weaveIncomingCharacteristic.properties, CBCharacteristicPropertyWrite, + @"Wrong property for weave incoming characteristic."); + XCTAssertEqual(weaveIncomingCharacteristic.permissions, CBAttributePermissionsWriteable, + @"Wrong permission for weave incoming characteristic."); +} + +#pragma mark - Socket connection + +- (void)checkOpenSocketWithShouldAccept:(BOOL)shouldAccept { + [self checkOpenSocketWithShouldAccept:shouldAccept central:nil]; +} + +- (void)checkOpenSocketWithShouldAccept:(BOOL)shouldAccept + central:(CBCentral *)central { + CBMutableCharacteristic *characteristic; + NSMutableData *data = [NSMutableData data]; + _shouldAcceptSocket = shouldAccept; + + // This is necessary to ensure that negotiated packet size is |_packetSize|. It should be set here + // and in the connection request packet. + _centralMaximumUpdateValueLength = _packetSize; + characteristic = _peripheralServiceManager.weaveIncomingCharacteristic; + GNSWeaveConnectionRequestPacket *connectionRequest = + [[GNSWeaveConnectionRequestPacket alloc] initWithMinVersion:1 + maxVersion:1 + maxPacketSize:_packetSize + data:nil]; + [data appendData:[connectionRequest serialize]]; + XCTAssertNotNil(characteristic); + CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]); + OCMStub([request value]).andReturn(data); + [self setupRequest:request + withCharacteristic:characteristic + central:central]; + __block GNSUpdateValueHandler updateValueHandler = nil; + // Nothing is sent when the socket is refused in the Weave protocol. + if (!shouldAccept) { + return; + } + OCMExpect([_peripheralManagerMock + updateOutgoingCharOnSocket:[OCMArg checkWithBlock:^BOOL(id obj) { + // The socket has not been created yet. So the socket check has to be done with a block. + return _receivedSocket == obj; + }] + withHandler:[OCMArg checkWithBlock:^(id handler) { + updateValueHandler = [handler copy]; + return YES; + }]]); + [_peripheralServiceManager processWriteRequest:request]; + XCTAssertNotNil(updateValueHandler); + if (!updateValueHandler) { + return; + } + NSMutableData *expectedData = [NSMutableData data]; + GNSWeaveConnectionConfirmPacket *connectionConfirm = + [[GNSWeaveConnectionConfirmPacket alloc] initWithVersion:1 packetSize:_packetSize data:nil]; + [expectedData appendData:[connectionConfirm serialize]]; + OCMExpect( + [_peripheralManagerMock updateOutgoingCharacteristic:expectedData onSocket:_receivedSocket]) + .andReturn(YES); + XCTAssertNotNil(_receivedSocket); + XCTAssertFalse(_receivedSocket.isConnected); + if (shouldAccept) { + OCMExpect([_socketDelegateMock socketDidConnect:_receivedSocket]); + } + updateValueHandler(_peripheralManagerMock); + XCTAssertEqual(_receivedSocket.isConnected, shouldAccept); + OCMVerifyAll((id)request); +} + +- (void)testRefuseSocket { + [self checkOpenSocketWithShouldAccept:NO]; +} + +- (void)testAcceptSocket { + [self checkOpenSocketWithShouldAccept:YES]; +} + +- (void)testTwoConnectionRequests { + [self checkOpenSocketWithShouldAccept:YES]; + GNSSocket *firstSocket = _receivedSocket; + OCMExpect([_socketDelegateMock socket:firstSocket + didDisconnectWithError:[OCMArg checkWithBlock:^BOOL(NSError *error) { + return [error.domain isEqualToString:kGNSSocketsErrorDomain] && + error.code == GNSErrorNewInviteToConnectReceived; + }]]); + OCMExpect([_peripheralManagerMock socketDidDisconnect:firstSocket]); + [self checkOpenSocketWithShouldAccept:YES + central:firstSocket.peerAsCentral]; + XCTAssertNotEqual(firstSocket, _receivedSocket); + XCTAssertFalse(firstSocket.isConnected); +} + +#pragma mark - Socket receive data + +- (NSData *)generateDataWithSize:(uint32_t)length { + NSMutableData *result = [NSMutableData dataWithCapacity:length]; + unsigned char byte = 0; + for (NSInteger ii = 0; ii < length; ii++) { + [result appendBytes:&byte length:sizeof(byte)]; + byte++; + } + return result; +} + +- (void)testReceiveEmptyMessage { + [self checkOpenSocketWithShouldAccept:YES]; + + NSData *expectedMessage = [self generateDataWithSize:0]; + OCMExpect([_socketDelegateMock socket:_receivedSocket didReceiveData:expectedMessage]); + + GNSWeaveDataPacket *dataPacket = + [[GNSWeaveDataPacket alloc] initWithPacketCounter:_receivedSocket.receivePacketCounter + firstPacket:YES + lastPacket:YES + data:expectedMessage]; + CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]); + OCMStub([request value]).andReturn([dataPacket serialize]); + [self setupRequest:request + withCharacteristic:_peripheralServiceManager.weaveIncomingCharacteristic + central:_receivedSocket.peerAsCentral]; + [_peripheralServiceManager processWriteRequest:request]; + XCTAssertTrue(_receivedSocket.isConnected); + OCMVerifyAll((id)request); +} + +- (void)simulateReceiveMessageWithSize:(uint32_t)size + packetSize:(NSUInteger)packetSize + counter:(UInt8)counter { + NSData *expectedMessage = [self generateDataWithSize:size]; + OCMExpect([_socketDelegateMock socket:_receivedSocket didReceiveData:expectedMessage]); + + NSUInteger offset = 0; + UInt8 receivePacketCounter = counter; + while (offset < size) { + GNSWeaveDataPacket *dataPacket = + [GNSWeaveDataPacket dataPacketWithPacketCounter:receivePacketCounter + packetSize:packetSize + data:expectedMessage + offset:&offset]; + CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]); + OCMStub([request value]).andReturn([dataPacket serialize]); + [self setupRequest:request + withCharacteristic:_peripheralServiceManager.weaveIncomingCharacteristic + central:_receivedSocket.peerAsCentral]; + [_peripheralServiceManager processWriteRequest:request]; + XCTAssertTrue(_receivedSocket.isConnected); + OCMVerifyAll((id)request); + receivePacketCounter = (receivePacketCounter + 1) % kGNSMaxPacketCounterValue; + } +} + +- (void)testReceiveSinglePacketMessage { + _packetSize = 100; + [self checkOpenSocketWithShouldAccept:YES]; + [self simulateReceiveMessageWithSize:99 + packetSize:_packetSize + counter:_receivedSocket.receivePacketCounter]; +} + +- (void)testReceiveTwoSinglePacketMessages { + _packetSize = 100; + [self checkOpenSocketWithShouldAccept:YES]; + [self simulateReceiveMessageWithSize:1 + packetSize:_packetSize + counter:_receivedSocket.receivePacketCounter]; + [self simulateReceiveMessageWithSize:98 + packetSize:_packetSize + counter:_receivedSocket.receivePacketCounter]; +} + +- (void)testReceiveMultiplePacketMessage { + _packetSize = 100; + [self checkOpenSocketWithShouldAccept:YES]; + [self simulateReceiveMessageWithSize:101 + packetSize:_packetSize + counter:_receivedSocket.receivePacketCounter]; +} + +- (void)testReceiveSeveralMultiplePacketMessage { + _packetSize = 100; + [self checkOpenSocketWithShouldAccept:YES]; + [self simulateReceiveMessageWithSize:101 + packetSize:_packetSize + counter:_receivedSocket.receivePacketCounter]; + [self simulateReceiveMessageWithSize:198 + packetSize:_packetSize + counter:_receivedSocket.receivePacketCounter]; + [self simulateReceiveMessageWithSize:1000 + packetSize:_packetSize + counter:_receivedSocket.receivePacketCounter]; +} + +#pragma mark - Socket send data + +- (BOOL)simulateSendMessageWithExpectedData:(NSData *)expectedData + packetSize:(NSUInteger)packetSize + completion:(GNSErrorHandler)completion { + BOOL sendHasBeenCompleted = YES; + // The expectedData.length divided by the amount of data that fits a packet, rounded up. + NSUInteger expectedPacketNumber = + (expectedData.length / (packetSize - 1)) + (expectedData.length % (packetSize - 1) != 0); + if (expectedData.length == 0) { + expectedPacketNumber = 1; + } + _packetSize = packetSize; + + [self checkOpenSocketWithShouldAccept:YES]; + UInt8 sendPacketCounter = _receivedSocket.sendPacketCounter; + + __block GNSUpdateValueHandler updateValueHandler = nil; + OCMStub([_peripheralManagerMock updateOutgoingCharOnSocket:_receivedSocket + withHandler:[OCMArg checkWithBlock:^(id obj) { + updateValueHandler = obj; + return YES; + }]]); + // Send the data + [_receivedSocket sendData:expectedData progressHandler:nil completion:completion]; + XCTAssertNotNil(updateValueHandler); + if (!updateValueHandler) { + return NO; + } + __block NSData *packetSent = nil; + OCMStub([_peripheralManagerMock + updateOutgoingCharacteristic:[OCMArg checkWithBlock:^BOOL(id obj) { + packetSent = obj; + return YES; + }] + onSocket:_receivedSocket]) + .andReturn(YES); + + NSMutableData *sentData = [NSMutableData data]; + for (NSUInteger i = 0; i < expectedPacketNumber; i++) { + // Cleanup |updateValueHandler| before calling it. So a new handler can be received if needed. + GNSUpdateValueHandler tmpUpdateValueHandler = updateValueHandler; + updateValueHandler = nil; + XCTAssertNotNil(tmpUpdateValueHandler); + tmpUpdateValueHandler(_peripheralManagerMock); + NSError *error = nil; + XCTAssertLessThanOrEqual(packetSent.length, packetSize); + GNSWeavePacket *packet = [GNSWeavePacket parseData:packetSent error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(packet); + XCTAssertTrue([packet isKindOfClass:[GNSWeaveDataPacket class]]); + GNSWeaveDataPacket *dataPacket = (GNSWeaveDataPacket *)packet; + NSLog(@"packetCounter = %d", dataPacket.packetCounter); + XCTAssertEqual(dataPacket.packetCounter, sendPacketCounter); + sendPacketCounter = (sendPacketCounter + 1) % kGNSMaxPacketCounterValue; + if (i == 0) { + XCTAssertTrue(dataPacket.isFirstPacket); + } else { + XCTAssertFalse(dataPacket.isFirstPacket); + } + [sentData appendData:dataPacket.data]; + if (i == expectedPacketNumber - 1) { + XCTAssertTrue(dataPacket.isLastPacket); + } else { + XCTAssertFalse(dataPacket.isLastPacket); + } + } + XCTAssertEqual((int)_receivedSocket.sendPacketCounter, (int)sendPacketCounter); + if (sendHasBeenCompleted) { + XCTAssertEqualObjects(sentData, expectedData); + } + return sendHasBeenCompleted; +} + +- (void)simulateMessageWithSize:(uint32_t)size packetSize:(NSUInteger)packetSize { + __block NSInteger completionCalled = 0; + NSData *expectedData = [self generateDataWithSize:size]; + BOOL completed = [self simulateSendMessageWithExpectedData:expectedData + packetSize:packetSize + completion:^(NSError *error) { + XCTAssertNil(error); + completionCalled++; + }]; + XCTAssertTrue(completed); + XCTAssertEqual(completionCalled, 1); +} + +- (void)testSendEmptyMessage { + [self simulateMessageWithSize:0 packetSize:30]; +} + +// The header has 1 byte. +- (void)testSendSinglePacketMessage { + [self simulateMessageWithSize:29 packetSize:30]; +} + +// The header has 1 byte. +- (void)testSendTwoPacketMessage { + [self simulateMessageWithSize:30 packetSize:30]; +} + +- (void)testSendMessagesWithSeveralPacketSizes { + for (NSUInteger packetSize = 20; packetSize < 50; packetSize++) { + [self simulateMessageWithSize:200 packetSize:packetSize]; + } +} + +- (void)testSendMessagesWithSeveralSizes { + for (uint32_t size = 100; size < 1000; size += 50) { + [self simulateMessageWithSize:size packetSize:120]; + } +} + +#pragma mark - Socket disconnect + +- (void)testUnsubscribeToDisconnect { + [self checkOpenSocketWithShouldAccept:YES]; + XCTAssertTrue(_receivedSocket.isConnected); + // Unsubscribing to the incoming characteristic: nothing should happen. + [_peripheralServiceManager central:_receivedSocket.peerAsCentral + didUnsubscribeFromCharacteristic:_peripheralServiceManager.weaveIncomingCharacteristic]; + XCTAssertTrue(_receivedSocket.isConnected); + // Unsubscribing to the outgoing characteristic: the socket should be disconnected. + OCMExpect([_socketDelegateMock socket:_receivedSocket didDisconnectWithError:nil]); + OCMExpect([_peripheralManagerMock socketDidDisconnect:_receivedSocket]); + [_peripheralServiceManager central:_receivedSocket.peerAsCentral + didUnsubscribeFromCharacteristic:_peripheralServiceManager.weaveOutgoingCharacteristic]; + XCTAssertFalse(_receivedSocket.isConnected); +} + +- (void)testDisconnect { + [self checkOpenSocketWithShouldAccept:YES]; + __block GNSUpdateValueHandler updateValueHandler = nil; + OCMExpect([_peripheralManagerMock + updateOutgoingCharOnSocket:_receivedSocket + withHandler:[OCMArg checkWithBlock:^(id handler) { + updateValueHandler = handler; + return YES; + }]]); + OCMExpect([_peripheralManagerMock socketDidDisconnect:_receivedSocket]); + UInt8 sendPacketCounter = _receivedSocket.sendPacketCounter; + [_receivedSocket disconnect]; + XCTAssertNotNil(updateValueHandler); + if (!updateValueHandler) { + return; + } + GNSWeaveErrorPacket *errorPacket = + [[GNSWeaveErrorPacket alloc] initWithPacketCounter:sendPacketCounter]; + OCMExpect([_peripheralManagerMock updateOutgoingCharacteristic:[errorPacket serialize] + onSocket:_receivedSocket]) + .andReturn(YES); + OCMExpect([_socketDelegateMock socket:_receivedSocket didDisconnectWithError:nil]); + XCTAssertEqual(updateValueHandler(_peripheralManagerMock), GNSOutgoingCharUpdateNoReschedule); + XCTAssertFalse(_receivedSocket.isConnected); +} + +- (void)testDisconnectWithErrorToSendDisconnectPacket { + [self checkOpenSocketWithShouldAccept:YES]; + __block GNSUpdateValueHandler updateValueHandler = nil; + OCMExpect([_peripheralManagerMock + updateOutgoingCharOnSocket:_receivedSocket + withHandler:[OCMArg checkWithBlock:^(id handler) { + updateValueHandler = handler; + return YES; + }]]); + OCMExpect([_peripheralManagerMock socketDidDisconnect:_receivedSocket]); + UInt8 sendPacketCounter = _receivedSocket.sendPacketCounter; + [_receivedSocket disconnect]; + XCTAssertNotNil(updateValueHandler); + if (!updateValueHandler) { + return; + } + GNSWeaveErrorPacket *errorPacket = + [[GNSWeaveErrorPacket alloc] initWithPacketCounter:sendPacketCounter]; + OCMExpect([_peripheralManagerMock updateOutgoingCharacteristic:[errorPacket serialize] + onSocket:_receivedSocket]) + .andReturn(NO); + XCTAssertEqual(updateValueHandler(_peripheralManagerMock), GNSOutgoingCharUpdateScheduleLater); + XCTAssertTrue(_receivedSocket.isConnected); + OCMExpect([_peripheralManagerMock updateOutgoingCharacteristic:[errorPacket serialize] + onSocket:_receivedSocket]) + .andReturn(YES); + OCMExpect([_socketDelegateMock socket:_receivedSocket didDisconnectWithError:nil]); + XCTAssertEqual(updateValueHandler(_peripheralManagerMock), GNSOutgoingCharUpdateNoReschedule); + XCTAssertFalse(_receivedSocket.isConnected); +} + +- (void)testSubscribeToOutgoingCharacteristic { + id centralMock = OCMStrictClassMock([CBCentral class]); + id characteristic = OCMStrictClassMock([CBMutableCharacteristic class]); + CBUUID *uuid = _peripheralServiceManager.weaveOutgoingCharacteristic.UUID; + OCMStub([characteristic UUID]).andReturn(uuid); + OCMStub([_cbPeripheralManagerMock + setDesiredConnectionLatency:CBPeripheralManagerConnectionLatencyLow + forCentral:centralMock]); + [_peripheralServiceManager central:centralMock didSubscribeToCharacteristic:characteristic]; + OCMVerifyAll(centralMock); +} + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Shared/GNSSocketTest.m b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Shared/GNSSocketTest.m new file mode 100644 index 00000000000..5a8f0ddadc9 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Shared/GNSSocketTest.m @@ -0,0 +1,355 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import <XCTest/XCTest.h> + +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSSocket+Private.h" +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSWeavePacket.h" +#import "third_party/objective_c/ocmock/v3/Source/OCMock/OCMock.h" + +@interface GNSSocketTest : XCTestCase { + GNSSocket *_socket; + id<GNSSocketOwner> _socketOwner; + CBCentral *_centralPeerMock; + id<GNSSocketDelegate> _socketDelegate; +} +@end + +@implementation GNSSocketTest + +- (void)setUp { + _centralPeerMock = OCMStrictClassMock([CBCentral class]); + NSUUID *identifier = [NSUUID UUID]; + OCMStub([_centralPeerMock identifier]).andReturn(identifier); + _socketOwner = [OCMStrictProtocolMock(@protocol(GNSSocketOwner)) noRetainObjectArgs]; + _socket = [[GNSSocket alloc] initWithOwner:_socketOwner centralPeer:_centralPeerMock]; + _socketDelegate = [OCMStrictProtocolMock(@protocol(GNSSocketDelegate)) noRetainObjectArgs]; + _socket.delegate = _socketDelegate; + XCTAssertFalse(_socket.connected); +} + +- (void)tearDown { + __weak GNSSocket *weakSocket = _socket; + OCMExpect([_socketOwner socketWillBeDeallocated:[OCMArg isNotNil]]); + _socket = nil; + XCTAssertNil(weakSocket); + OCMVerifyAll((id)_centralPeerMock); + OCMVerifyAll((id)_socketOwner); + OCMVerifyAll((id)_socketDelegate); +} + +- (void)connectSocket { + OCMExpect([_socketDelegate socketDidConnect:_socket]); + [_socket didConnect]; + XCTAssertTrue(_socket.connected); +} + +- (void)testPeripheralPeer { + CBPeripheral *peripheral = OCMStrictClassMock([CBPeripheral class]); + GNSSocket *socket = [[GNSSocket alloc] initWithOwner:_socketOwner peripheralPeer:peripheral]; + XCTAssertEqual(socket.peerAsPeripheral, peripheral); + OCMExpect([_socketOwner socketWillBeDeallocated:[OCMArg isNotNil]]); +} + +- (void)testCentralPeer { + CBCentral *central = OCMStrictClassMock([CBCentral class]); + GNSSocket *socket = [[GNSSocket alloc] initWithOwner:_socketOwner centralPeer:central]; + XCTAssertEqual(socket.peerAsCentral, central); + OCMExpect([_socketOwner socketWillBeDeallocated:[OCMArg isNotNil]]); +} + +- (void)testDisconnect { + [self connectSocket]; + OCMExpect([_socketOwner disconnectSocket:_socket]); + [_socket disconnect]; + XCTAssertTrue(_socket.connected); + OCMExpect([_socketDelegate socket:_socket didDisconnectWithError:nil]); + [_socket didDisconnectWithError:nil]; + XCTAssertFalse(_socket.connected); +} + +- (void)testDisconnectWithError { + [self connectSocket]; + OCMExpect([_socketOwner disconnectSocket:_socket]); + [_socket disconnect]; + XCTAssertTrue(_socket.connected); + NSError *error = [NSError errorWithDomain:@"domain" code:-42 userInfo:nil]; + OCMExpect([_socketDelegate socket:_socket didDisconnectWithError:error]); + [_socket didDisconnectWithError:error]; + XCTAssertFalse(_socket.connected); +} + +#pragma mark - Receive Data + +- (void)testReceiveDataWithOnePacket { + [self connectSocket]; + XCTAssertFalse([_socket waitingForIncomingData]); + NSData *data = [@"Some data to receive" dataUsingEncoding:NSUTF8StringEncoding]; + OCMExpect([_socketDelegate socket:_socket didReceiveData:data]); + GNSWeaveDataPacket *packet = + [[GNSWeaveDataPacket alloc] initWithPacketCounter:1 firstPacket:YES lastPacket:YES data:data]; + [_socket didReceiveIncomingWeaveDataPacket:packet]; + XCTAssertFalse([_socket waitingForIncomingData]); +} + +- (void)testReceiveDataWithTwoPackets { + [self connectSocket]; + XCTAssertFalse([_socket waitingForIncomingData]); + NSData *firstChunk = [@"Some data " dataUsingEncoding:NSUTF8StringEncoding]; + NSData *secondChunk = [@"to receive" dataUsingEncoding:NSUTF8StringEncoding]; + NSMutableData *totalData = [NSMutableData data]; + [totalData appendData:firstChunk]; + GNSWeaveDataPacket *firstPacket = [[GNSWeaveDataPacket alloc] initWithPacketCounter:1 + firstPacket:YES + lastPacket:NO + data:firstChunk]; + [_socket didReceiveIncomingWeaveDataPacket:firstPacket]; + XCTAssertTrue([_socket waitingForIncomingData]); + OCMExpect([_socketDelegate socket:_socket didReceiveData:totalData]); + [totalData appendData:secondChunk]; + GNSWeaveDataPacket *secondPacket = [[GNSWeaveDataPacket alloc] initWithPacketCounter:1 + firstPacket:NO + lastPacket:YES + data:secondChunk]; + [_socket didReceiveIncomingWeaveDataPacket:secondPacket]; + XCTAssertFalse([_socket waitingForIncomingData]); +} + +- (void)testReceiveDataWithOneDataPacketAndDisconnect { + [self connectSocket]; + XCTAssertFalse([_socket waitingForIncomingData]); + NSData *firstChunk = [@"Some data " dataUsingEncoding:NSUTF8StringEncoding]; + GNSWeaveDataPacket *firstPacket = [[GNSWeaveDataPacket alloc] initWithPacketCounter:1 + firstPacket:YES + lastPacket:NO + data:firstChunk]; + [_socket didReceiveIncomingWeaveDataPacket:firstPacket]; + XCTAssertTrue([_socket waitingForIncomingData]); + OCMExpect([_socketDelegate socket:_socket didDisconnectWithError:nil]); + [_socket didDisconnectWithError:nil]; + XCTAssertFalse(_socket.connected); + XCTAssertFalse([_socket waitingForIncomingData]); +} + +#pragma mark - Send Data + +- (void)testSendDataWithOnePacket { + _socket.packetSize = 100; + UInt8 sendPacketCounter = _socket.sendPacketCounter; + [self connectSocket]; + NSData *data = [@"Some data to send" dataUsingEncoding:NSUTF8StringEncoding]; + OCMStub([_socketOwner socketMaximumUpdateValueLength:[self checkSocketBlock]]).andReturn(100); + __block GNSUpdateValueHandler handler = nil; + OCMExpect([_socketOwner socket:_socket + addOutgoingCharUpdateHandler:[OCMArg checkWithBlock:^BOOL(id obj) { + handler = obj; + return YES; + }]]); + __block BOOL completionCalledCount = 0; + __block BOOL progressHandlerCount = 0; + [_socket sendData:data + progressHandler:^void(float progress) { + progressHandlerCount++; + } + completion:^(NSError *error) { + XCTAssertNil(error); + completionCalledCount++; + }]; + XCTAssertTrue([_socket isSendOperationInProgress]); + XCTAssertNotNil(handler); + if (!handler) { + return; + } + NSUInteger offset = 0; + GNSWeaveDataPacket *packet = [GNSWeaveDataPacket dataPacketWithPacketCounter:sendPacketCounter + packetSize:_socket.packetSize + data:data + offset:&offset]; + NSData *packetData = [packet serialize]; + OCMExpect([_socketOwner socket:_socket sendData:packetData]).andReturn(YES); + XCTAssertEqual(handler(), GNSOutgoingCharUpdateNoReschedule); + XCTAssertEqual(completionCalledCount, 1); + XCTAssertEqual(progressHandlerCount, 1); + XCTAssertFalse([_socket isSendOperationInProgress]); +} + +- (void)testSendDataWithTwoPackets { + _socket.packetSize = 20; + UInt8 sendPacketCounter = _socket.sendPacketCounter; + [self connectSocket]; + NSData *data = [@"Some larger data to send" dataUsingEncoding:NSUTF8StringEncoding]; + OCMStub([_socketOwner socketMaximumUpdateValueLength:[self checkSocketBlock]]).andReturn(20); + __block GNSUpdateValueHandler handler = nil; + OCMExpect([_socketOwner socket:_socket + addOutgoingCharUpdateHandler:[OCMArg checkWithBlock:^BOOL(id obj) { + handler = obj; + return YES; + }]]); + __block BOOL completionCalledCount = 0; + [_socket sendData:data progressHandler:nil completion:^(NSError *error) { + XCTAssertNil(error); + completionCalledCount++; + }]; + XCTAssertNotNil(handler); + if (!handler) { + return; + } + NSUInteger offset = 0; + GNSWeaveDataPacket *firstPacket = + [GNSWeaveDataPacket dataPacketWithPacketCounter:sendPacketCounter + packetSize:_socket.packetSize + data:data + offset:&offset]; + NSData *firstPacketData = [firstPacket serialize]; + OCMExpect([_socketOwner socket:_socket sendData:firstPacketData]).andReturn(YES); + OCMExpect([_socketOwner socket:_socket + addOutgoingCharUpdateHandler:[OCMArg checkWithBlock:^BOOL(id obj) { + handler = obj; + return YES; + }]]); + XCTAssertEqual(handler(), GNSOutgoingCharUpdateNoReschedule); + XCTAssertNotNil(handler); + if (!handler) { + return; + } + XCTAssertEqual(completionCalledCount, 0); + + GNSWeaveDataPacket *secondPacket = + [GNSWeaveDataPacket dataPacketWithPacketCounter:sendPacketCounter + 1 + packetSize:_socket.packetSize + data:data + offset:&offset]; + NSData *secondPacketData = [secondPacket serialize]; + OCMExpect([_socketOwner socket:_socket sendData:secondPacketData]).andReturn(YES); + XCTAssertEqual(handler(), GNSOutgoingCharUpdateNoReschedule); + XCTAssertEqual(completionCalledCount, 1); + XCTAssertFalse([_socket isSendOperationInProgress]); +} + +- (void)testSendDataWithBluetoothRetry { + _socket.packetSize = 100; + UInt8 sentPacketCounter = _socket.sendPacketCounter; + [self connectSocket]; + NSData *data = [@"Some data to send" dataUsingEncoding:NSUTF8StringEncoding]; + OCMStub([_socketOwner socketMaximumUpdateValueLength:[self checkSocketBlock]]).andReturn(100); + __block GNSUpdateValueHandler handler = nil; + OCMExpect([_socketOwner socket:_socket + addOutgoingCharUpdateHandler:[OCMArg checkWithBlock:^BOOL(id obj) { + handler = obj; + return YES; + }]]); + __block BOOL completionCalledCount = 0; + [_socket sendData:data progressHandler:nil completion:^(NSError *error) { + XCTAssertNil(error); + completionCalledCount++; + }]; + XCTAssertTrue([_socket isSendOperationInProgress]); + XCTAssertNotNil(handler); + if (!handler) { + return; + } + NSUInteger offset = 0; + GNSWeaveDataPacket *packet = [GNSWeaveDataPacket dataPacketWithPacketCounter:sentPacketCounter + packetSize:_socket.packetSize + data:data + offset:&offset]; + NSData *packetData = [packet serialize]; + OCMExpect([_socketOwner socket:_socket sendData:packetData]).andReturn(NO); + XCTAssertEqual(handler(), GNSOutgoingCharUpdateScheduleLater); + XCTAssertEqual(completionCalledCount, 0); + XCTAssertTrue([_socket isSendOperationInProgress]); + OCMExpect([_socketOwner socket:_socket sendData:packetData]).andReturn(YES); + XCTAssertEqual(handler(), GNSOutgoingCharUpdateNoReschedule); + XCTAssertEqual(completionCalledCount, 1); + XCTAssertFalse([_socket isSendOperationInProgress]); +} + +- (void)testDisconnectAndSendDataWithOneChunk { + [self connectSocket]; + OCMExpect([_socketDelegate socket:_socket didDisconnectWithError:nil]); + [_socket didDisconnectWithError:nil]; + XCTAssertFalse(_socket.connected); + NSData *data = [@"Some data to send" dataUsingEncoding:NSUTF8StringEncoding]; + OCMStub([_socketOwner socketMaximumUpdateValueLength:[self checkSocketBlock]]).andReturn(100); + __block BOOL completionCalledCount = 0; + [_socket sendData:data progressHandler:nil completion:^(NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, kGNSSocketsErrorDomain); + XCTAssertEqual(error.code, GNSErrorNoConnection); + completionCalledCount++; + }]; + XCTAssertEqual(completionCalledCount, 1); + XCTAssertFalse([_socket isSendOperationInProgress]); +} + +- (void)testSendDataWithTwoChunksAndDisconnectBetweenChunk { + _socket.packetSize = 20; + UInt8 sentPacketCounter = _socket.sendPacketCounter; + [self connectSocket]; + NSData *data = [@"Some larger data to send" dataUsingEncoding:NSUTF8StringEncoding]; + OCMStub([_socketOwner socketMaximumUpdateValueLength:[self checkSocketBlock]]).andReturn(20); + __block GNSUpdateValueHandler handler = nil; + OCMExpect([_socketOwner socket:_socket + addOutgoingCharUpdateHandler:[OCMArg checkWithBlock:^BOOL(id obj) { + handler = obj; + return YES; + }]]); + __block BOOL completionCalledCount = 0; + [_socket sendData:data progressHandler:nil completion:^(NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, kGNSSocketsErrorDomain); + XCTAssertEqual(error.code, GNSErrorNoConnection); + completionCalledCount++; + }]; + XCTAssertNotNil(handler); + if (!handler) { + return; + } + NSUInteger offset = 0; + GNSWeaveDataPacket *packet = [GNSWeaveDataPacket dataPacketWithPacketCounter:sentPacketCounter + packetSize:_socket.packetSize + data:data + offset:&offset]; + NSData *packetData = [packet serialize]; + OCMExpect([_socketOwner socket:_socket sendData:packetData]).andReturn(YES); + OCMExpect([_socketOwner socket:_socket + addOutgoingCharUpdateHandler:[OCMArg checkWithBlock:^BOOL(id obj) { + handler = obj; + return YES; + }]]); + XCTAssertEqual(handler(), GNSOutgoingCharUpdateNoReschedule); + XCTAssertNotNil(handler); + if (!handler) { + return; + } + XCTAssertEqual(completionCalledCount, 0); + OCMExpect([_socketDelegate socket:_socket didDisconnectWithError:nil]); + [_socket didDisconnectWithError:nil]; + XCTAssertFalse(_socket.connected); + XCTAssertEqual(handler(), GNSOutgoingCharUpdateNoReschedule); + XCTAssertEqual(completionCalledCount, 1); + XCTAssertFalse([_socket isSendOperationInProgress]); +} + +#pragma mark - Helpers + +- (id)checkSocketBlock { + // This method create a OCMArg without retaining the parameter. + __weak GNSSocket *weakSocket = _socket; + return [OCMArg checkWithBlock:^BOOL(id object) { + return object == weakSocket; + }]; +} + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Shared/GNSWeavePacketTest.m b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Shared/GNSWeavePacketTest.m new file mode 100644 index 00000000000..49534e93226 --- /dev/null +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Shared/GNSWeavePacketTest.m @@ -0,0 +1,310 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import <XCTest/XCTest.h> + +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSWeavePacket.h" + +#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSUtils.h" + +@interface GNSWeavePacketTest : XCTestCase { + NSData *_largeNonEmptyData; + NSData *_smallNonEmptyData; +} +@end + +@implementation GNSWeavePacketTest + +- (void)setUp { + _largeNonEmptyData = + [@"Some non-empty data data is larger than 20 bytes." dataUsingEncoding:NSUTF8StringEncoding]; + _smallNonEmptyData = [@"Small data" dataUsingEncoding:NSUTF8StringEncoding]; +} + +- (NSData *)weavePacketWithHeader:(UInt8)header data:(NSData *)data { + NSMutableData *dataPacket = [[NSMutableData alloc] init]; + [dataPacket appendBytes:&header length:sizeof(header)]; + if (data != nil) { + [dataPacket appendData:[data subdataWithRange:NSMakeRange(0, data.length)]]; + } + return dataPacket; +} + +- (void)testParsePacketCounter { + for (UInt8 packetCounter = 0; packetCounter < 8; packetCounter++) { + UInt8 header = (packetCounter + << 4); // 0111 0000, the packetCounter value is contained on 5, 6, and 7 bits. + NSError *error = nil; + GNSWeavePacket *parsedPacket = + [GNSWeavePacket parseData:[self weavePacketWithHeader:header data:_largeNonEmptyData] + error:&error]; + XCTAssertNotNil(parsedPacket); + XCTAssertEqual(packetCounter, parsedPacket.packetCounter); + } +} + +- (void)testParseEmptyPacket { + NSData *data = [NSData data]; + NSError *error = nil; + GNSWeavePacket *parsedPacket = [GNSWeavePacket parseData:data error:&error]; + XCTAssertNil(parsedPacket); + XCTAssertNotNil(error); + XCTAssertEqual(error.domain, kGNSSocketsErrorDomain); + XCTAssertEqual(error.code, GNSErrorParsingWeavePacketTooSmall); +} + +- (void)testParsingControlPacketTooLarge { + UInt8 header = (1 << 7); // 1000 0000 + NSError *error = nil; + GNSWeavePacket *parsedPacket = + [GNSWeavePacket parseData:[self weavePacketWithHeader:header data:_largeNonEmptyData] + error:&error]; + XCTAssertNil(parsedPacket); + XCTAssertNotNil(error); + XCTAssertEqual(error.domain, kGNSSocketsErrorDomain); + XCTAssertEqual(error.code, GNSErrorParsingWeavePacketTooLarge); +} + +- (void)testParseFirstDataPacket { + UInt8 header = (1 << 3); // 0000 1000, the packet counter is zero + NSError *error = nil; + GNSWeavePacket *parsedPacket = + [GNSWeavePacket parseData:[self weavePacketWithHeader:header data:_largeNonEmptyData] + error:&error]; + XCTAssertNotNil(parsedPacket); + XCTAssertTrue([parsedPacket isKindOfClass:[GNSWeaveDataPacket class]]); + GNSWeaveDataPacket *parsedDataPacket = (GNSWeaveDataPacket *)parsedPacket; + XCTAssertTrue(parsedDataPacket.isFirstPacket); + XCTAssertFalse(parsedDataPacket.isLastPacket); + XCTAssertEqualObjects(parsedDataPacket.data, _largeNonEmptyData); +} + +- (void)testParseLastDataPacket { + UInt8 header = (1 << 2); // 0000 0100, the packet counter is zero + NSError *error = nil; + GNSWeavePacket *parsedPacket = + [GNSWeavePacket parseData:[self weavePacketWithHeader:header data:_largeNonEmptyData] + error:&error]; + XCTAssertNotNil(parsedPacket); + XCTAssertTrue([parsedPacket isKindOfClass:[GNSWeaveDataPacket class]]); + GNSWeaveDataPacket *parsedDataPacket = (GNSWeaveDataPacket *)parsedPacket; + XCTAssertFalse(parsedDataPacket.isFirstPacket); + XCTAssertTrue(parsedDataPacket.isLastPacket); + XCTAssertEqualObjects(parsedDataPacket.data, _largeNonEmptyData); +} + +- (void)testParseOtherDataPacket { + UInt8 header = 0; // 0000 0000, the packet counter is zero + NSError *error = nil; + GNSWeavePacket *parsedPacket = + [GNSWeavePacket parseData:[self weavePacketWithHeader:header data:_largeNonEmptyData] + error:&error]; + XCTAssertNotNil(parsedPacket); + XCTAssertTrue([parsedPacket isKindOfClass:[GNSWeaveDataPacket class]]); + GNSWeaveDataPacket *parsedDataPacket = (GNSWeaveDataPacket *)parsedPacket; + XCTAssertFalse(parsedDataPacket.isFirstPacket); + XCTAssertFalse(parsedDataPacket.isLastPacket); + XCTAssertEqualObjects(parsedDataPacket.data, _largeNonEmptyData); +} + +- (void)testBuildAndParseMultipleDataPackets { + NSMutableData *receivedData = [[NSMutableData alloc] init]; + NSUInteger offset = 0; + UInt8 packetCounter = 0; + UInt16 packetSize = 20; + BOOL isLastPacket = NO; + while (offset < _largeNonEmptyData.length) { + GNSWeaveDataPacket *dataPacket = + [GNSWeaveDataPacket dataPacketWithPacketCounter:packetCounter + packetSize:packetSize + data:_largeNonEmptyData + offset:&offset]; + XCTAssertNotNil(dataPacket); + NSError *error = nil; + GNSWeavePacket *parsedPacket = [GNSWeavePacket parseData:[dataPacket serialize] error:&error]; + XCTAssertNotNil(parsedPacket); + XCTAssertTrue([parsedPacket isKindOfClass:[GNSWeaveDataPacket class]]); + GNSWeaveDataPacket *parsedDataPacket = (GNSWeaveDataPacket *)parsedPacket; + if (receivedData.length == 0) { + XCTAssertTrue(parsedDataPacket.isFirstPacket); + } + [receivedData appendData:[parsedDataPacket.data + subdataWithRange:NSMakeRange(0, parsedDataPacket.data.length)]]; + packetCounter++; + isLastPacket = parsedDataPacket.isLastPacket; + } + XCTAssertTrue(isLastPacket); + XCTAssertEqual(offset, _largeNonEmptyData.length); + XCTAssertEqualObjects(receivedData, _largeNonEmptyData); + XCTAssertEqualWithAccuracy(packetCounter, + ceil(_largeNonEmptyData.length / (float)(packetSize - 1)), 0.1f); +} + +- (void)testBuildAndParseSingleDataPacket { + NSUInteger offset = 0; + UInt8 packetCounter = 0; + UInt16 packetSize = 20; + GNSWeaveDataPacket *dataPacket = + [GNSWeaveDataPacket dataPacketWithPacketCounter:packetCounter + packetSize:packetSize + data:_smallNonEmptyData + offset:&offset]; + + XCTAssertNotNil(dataPacket); + NSError *error = nil; + GNSWeavePacket *parsedPacket = [GNSWeavePacket parseData:[dataPacket serialize] error:&error]; + XCTAssertNotNil(parsedPacket); + XCTAssertTrue([parsedPacket isKindOfClass:[GNSWeaveDataPacket class]]); + GNSWeaveDataPacket *parsedDataPacket = (GNSWeaveDataPacket *)parsedPacket; + XCTAssertTrue(parsedDataPacket.isLastPacket); + XCTAssertTrue(parsedDataPacket.isFirstPacket); + XCTAssertEqual(offset, _smallNonEmptyData.length); + XCTAssertEqualObjects(dataPacket.data, _smallNonEmptyData); +} + +- (void)testParseErrorControlPacket { + // 1000 0010, control packets have the MSB set and, error code is 2. The packet counter is 0. + UInt8 header = (1 << 7) + 2; + NSError *error = nil; + GNSWeavePacket *packet = + [GNSWeavePacket parseData:[self weavePacketWithHeader:header data:nil] error:&error]; + XCTAssertNotNil(packet); + XCTAssertTrue([packet isKindOfClass:[GNSWeaveErrorPacket class]]); + XCTAssertEqual(packet.packetCounter, 0); +} + +- (void)testBuildErrorControlPacket { + UInt8 packetCounter = 7; + GNSWeaveErrorPacket *errorPacket = + [[GNSWeaveErrorPacket alloc] initWithPacketCounter:packetCounter]; + NSData *serializedPacket = [errorPacket serialize]; + UInt8 firstByte = *(UInt8 *)serializedPacket.bytes; + // 1111 0010, control packet bit is set, counter is 7 (111) and error code is 2. + XCTAssertEqual(firstByte, (1 << 7) + (7 << 4) + 2); + + NSError *error = nil; + GNSWeavePacket *packet = [GNSWeavePacket parseData:serializedPacket error:&error]; + XCTAssertNotNil(packet); + XCTAssertTrue([packet isKindOfClass:[GNSWeaveErrorPacket class]]); + XCTAssertEqual(packet.packetCounter, packetCounter); +} + +- (void)testParseConnectionRequestControlPacket { + // 1000 0000, control packets have the most significat bit (MSB) set and, connection request code + // is 0. The packet counter is 0. + UInt8 header = (1 << 7) + 0; + NSMutableData *payload = [[NSMutableData alloc] init]; + UInt16 minVersion = 0; + UInt16 maxVersion = 3; + UInt16 maxPacketSize = 200; + + // The Weave protocol uses big-endian format for the multi-bytes types. + UInt16 minVersionBigEndian = CFSwapInt16HostToBig(minVersion); + UInt16 maxVersionBigEndian = CFSwapInt16HostToBig(maxVersion); + UInt16 maxPacketSizeBigEndian = CFSwapInt16HostToBig(maxPacketSize); + [payload appendBytes:&minVersionBigEndian length:sizeof(minVersionBigEndian)]; + [payload appendBytes:&maxVersionBigEndian length:sizeof(maxVersionBigEndian)]; + [payload appendBytes:&maxPacketSizeBigEndian length:sizeof(maxPacketSizeBigEndian)]; + NSError *error = nil; + GNSWeavePacket *packet = + [GNSWeavePacket parseData:[self weavePacketWithHeader:header data:payload] error:&error]; + XCTAssertNotNil(packet); + XCTAssertTrue([packet isKindOfClass:[GNSWeaveConnectionRequestPacket class]]); + XCTAssertEqual(packet.packetCounter, 0); + GNSWeaveConnectionRequestPacket *connectionRequestPacket = + (GNSWeaveConnectionRequestPacket *)packet; + XCTAssertEqual(connectionRequestPacket.minVersion, minVersion); + XCTAssertEqual(connectionRequestPacket.maxVersion, maxVersion); + XCTAssertEqual(connectionRequestPacket.maxPacketSize, maxPacketSize); + XCTAssertNil(connectionRequestPacket.data); +} + +- (void)testBuildConnectionRequestControlPacket { + UInt16 minVersion = 0; + UInt16 maxVersion = 3; + UInt16 maxPacketSize = 200; + NSData *smallPayload = [@"Payload" dataUsingEncoding:NSUTF8StringEncoding]; + GNSWeaveConnectionRequestPacket *requestPacket = + [[GNSWeaveConnectionRequestPacket alloc] initWithMinVersion:minVersion + maxVersion:maxVersion + maxPacketSize:maxPacketSize + data:smallPayload]; + NSData *serializedPacket = [requestPacket serialize]; + UInt8 firstByte = *(UInt8 *)serializedPacket.bytes; + XCTAssertEqual(firstByte, (1 << 7)); // 1000 0000 + + NSError *error = nil; + GNSWeavePacket *packet = [GNSWeavePacket parseData:serializedPacket error:&error]; + XCTAssertNotNil(packet); + XCTAssertTrue([packet isKindOfClass:[GNSWeaveConnectionRequestPacket class]]); + XCTAssertEqual(packet.packetCounter, 0); + GNSWeaveConnectionRequestPacket *connectionRequestPacket = + (GNSWeaveConnectionRequestPacket *)packet; + XCTAssertEqual(connectionRequestPacket.minVersion, minVersion); + XCTAssertEqual(connectionRequestPacket.maxVersion, maxVersion); + XCTAssertEqual(connectionRequestPacket.maxPacketSize, maxPacketSize); + XCTAssertEqualObjects(connectionRequestPacket.data, smallPayload); +} + +- (void)testParseConnectionConfirmControlPacket { + // 1000 0001, control packets have the MSB set and, connection request code is 1. The packet + // counter is 0. + UInt8 header = (1 << 7) + 1; + NSMutableData *payload = [[NSMutableData alloc] init]; + UInt16 version = 2; + UInt16 packetSize = 30; + + // The Weave protocol uses big-endian format for the multi-bytes types. + UInt16 versionBigEndian = CFSwapInt16HostToBig(version); + UInt16 packetSizeBigEndian = CFSwapInt16HostToBig(packetSize); + [payload appendBytes:&versionBigEndian length:sizeof(versionBigEndian)]; + [payload appendBytes:&packetSizeBigEndian length:sizeof(packetSizeBigEndian)]; + NSError *error = nil; + GNSWeavePacket *packet = + [GNSWeavePacket parseData:[self weavePacketWithHeader:header data:payload] error:&error]; + XCTAssertNotNil(packet); + XCTAssertTrue([packet isKindOfClass:[GNSWeaveConnectionConfirmPacket class]]); + XCTAssertEqual(packet.packetCounter, 0); + GNSWeaveConnectionConfirmPacket *connectionRequestPacket = + (GNSWeaveConnectionConfirmPacket *)packet; + XCTAssertEqual(connectionRequestPacket.version, version); + XCTAssertEqual(connectionRequestPacket.packetSize, packetSize); + XCTAssertNil(connectionRequestPacket.data); +} + +- (void)testBuildConnectionConfirmControlPacket { + UInt16 version = 30; + UInt16 packetSize = 21; + NSData *smallPayload = [@"Payload" dataUsingEncoding:NSUTF8StringEncoding]; + GNSWeaveConnectionConfirmPacket *confirmPacket = + [[GNSWeaveConnectionConfirmPacket alloc] initWithVersion:version + packetSize:packetSize + data:smallPayload]; + NSData *serializedPacket = [confirmPacket serialize]; + UInt8 firstByte = *(UInt8 *)serializedPacket.bytes; + XCTAssertEqual(firstByte, (1 << 7) + 1); // 1000 0001 + + NSError *error = nil; + GNSWeavePacket *packet = [GNSWeavePacket parseData:serializedPacket error:&error]; + XCTAssertNotNil(packet); + XCTAssertTrue([packet isKindOfClass:[GNSWeaveConnectionConfirmPacket class]]); + XCTAssertEqual(packet.packetCounter, 0); + GNSWeaveConnectionConfirmPacket *connectionConfirmPacket = + (GNSWeaveConnectionConfirmPacket *)packet; + XCTAssertEqual(connectionConfirmPacket.version, version); + XCTAssertEqual(connectionConfirmPacket.packetSize, packetSize); + XCTAssertEqualObjects(connectionConfirmPacket.data, smallPayload); +} + +@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Tests/GNCBLEATest.mm b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Tests/GNCBLEATest.mm deleted file mode 100644 index 3818d9f6a84..00000000000 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Tests/GNCBLEATest.mm +++ /dev/null @@ -1,22 +0,0 @@ -#import "internal/platform/implementation/ios/Tests/GNCBLEA.h" - -#import <XCTest/XCTest.h> - -@interface GNCBLEATest : XCTestCase -@end - -@implementation GNCBLEATest -- (void)setUp { - [super setUp]; - // Remove if not used. -} - -- (void)tearDown { - // Remove if not used. - [super tearDown]; -} - -- (void)testFoo { - XCTAssertTrue(YES, @"A true test"); -} -@end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Tests/GNCBleTest.mm b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Tests/GNCBleTest.mm index 27842e053e4..cb4b2b3ec00 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Tests/GNCBleTest.mm +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Tests/GNCBleTest.mm @@ -13,6 +13,7 @@ // limitations under the License. #import <XCTest/XCTest.h> +#include "internal/platform/implementation/ios/bluetooth_adapter.h" #include <string> #include <utility> @@ -28,7 +29,9 @@ using ::location::nearby::api::BluetoothAdapter; using ::location::nearby::api::ImplementationPlatform; using ::location::nearby::api::ble_v2::BleAdvertisementData; using ::location::nearby::api::ble_v2::BleMedium; +using ::location::nearby::api::ble_v2::GattCharacteristic; using ::location::nearby::api::ble_v2::TxPowerLevel; +using IOSBluetoothAdapter = ::location::nearby::ios::BluetoothAdapter; static const char *const kAdvertisementString = "\x0a\x0b\x0c\x0d"; static const TxPowerLevel kTxPowerLevel = TxPowerLevel::kHigh; @@ -36,6 +39,7 @@ static const TxPowerLevel kTxPowerLevel = TxPowerLevel::kHigh; @interface GNCBleTest : XCTestCase @end +// TODO(b/222392304): More tests on GNCBleTest. @implementation GNCBleTest { std::unique_ptr<BluetoothAdapter> _adapter; std::unique_ptr<BleMedium> _ble; @@ -57,7 +61,10 @@ static const TxPowerLevel kTxPowerLevel = TxPowerLevel::kHigh; advertising_data.service_data = {{service_uuid, advertisement_bytes}}; XCTAssertTrue(_ble->StartAdvertising(advertising_data, - {.tx_power_level = kTxPowerLevel, .is_connectable = true})); + {.tx_power_level = kTxPowerLevel, .is_connectable = true})); + + [NSThread sleepForTimeInterval:0.1]; + XCTAssertTrue(_ble->StopAdvertising()); } @@ -66,7 +73,39 @@ static const TxPowerLevel kTxPowerLevel = TxPowerLevel::kHigh; XCTAssertTrue(_ble->StartScanning(service_uuid, kTxPowerLevel, {})); + [NSThread sleepForTimeInterval:0.1]; + XCTAssertTrue(_ble->StopScanning()); } +- (void)testGattServerWorking { + // Test creating gatt_server. + auto gatt_server = _ble->StartGattServer(/*ServerGattConnectionCallback=*/{}); + XCTAssert(gatt_server != nullptr); + + // Test creating characteristic. + Uuid service_uuid(1234, 5678); + Uuid characteristic_uuid(5678, 1234); + std::vector<GattCharacteristic::Permission> permissions = {GattCharacteristic::Permission::kRead}; + std::vector<GattCharacteristic::Property> properties = {GattCharacteristic::Property::kRead}; + + // NOLINTNEXTLINE + absl::optional<GattCharacteristic> gatt_characteristic = + gatt_server->CreateCharacteristic(service_uuid, characteristic_uuid, permissions, properties); + XCTAssertTrue(gatt_characteristic.has_value()); + + // Test updating characteristic. + ByteArray any_byte("any"); + XCTAssertTrue(gatt_server->UpdateCharacteristic(gatt_characteristic.value(), any_byte)); + + gatt_server->Stop(); +} + +- (void)testCreateGattClient { + IOSBluetoothAdapter *adapter = static_cast<IOSBluetoothAdapter *>(_adapter.get()); + auto gatt_client = _ble->ConnectToGattServer(adapter->GetPeripheral(), kTxPowerLevel, {}); + + XCTAssert(gatt_client != nullptr); +} + @end diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Tests/GNCMultiThreadExecutorTest.mm b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Tests/GNCMultiThreadExecutorTest.mm index 32bd2e324b9..f27dd5c5a47 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/Tests/GNCMultiThreadExecutorTest.mm +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/Tests/GNCMultiThreadExecutorTest.mm @@ -51,11 +51,11 @@ using MultiThreadExecutor = location::nearby::api::SubmittableExecutor; Runnable incrementer = [self]() { self.counter++; }; for (int i = 0; i < kIncrements; i++) { executor->Execute(std::move(incrementer)); + [NSThread sleepForTimeInterval:0.01]; } // Check that the counter has the expected value after giving the runnables time to run. - [NSThread sleepForTimeInterval:0.01]; - XCTAssertLessThan(abs(self.counter - kIncrements), 3); + XCTAssertLessThanOrEqual(abs(self.counter - kIncrements), 0); } // Tests that the executor submits runnables as expected. @@ -67,11 +67,11 @@ using MultiThreadExecutor = location::nearby::api::SubmittableExecutor; Runnable incrementer = [self]() { self.counter++; }; for (int i = 0; i < kIncrements; i++) { executor->DoSubmit(std::move(incrementer)); + [NSThread sleepForTimeInterval:0.01]; } // Check that the counter has the expected value after giving the runnables time to run. - [NSThread sleepForTimeInterval:0.01]; - XCTAssertLessThan(abs(self.counter - kIncrements), 3); + XCTAssertLessThanOrEqual(abs(self.counter - kIncrements), 0); } // Tests that fails to submit when the executor is shut down. diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/ble.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/ble.h index ce0d0e28031..22be5ae1a73 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/ble.h +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/ble.h @@ -14,14 +14,18 @@ #ifndef THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_IOS_BLE_H_ #define THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_IOS_BLE_H_ +#ifdef __cplusplus +#import <CoreBluetooth/CoreBluetooth.h> #import <Foundation/Foundation.h> #include <string> +#include "absl/types/optional.h" #include "internal/platform/cancellation_flag.h" #include "internal/platform/implementation/ble_v2.h" #include "internal/platform/implementation/bluetooth_adapter.h" +#import "internal/platform/implementation/ios/Mediums/GNCMConnection.h" #include "internal/platform/implementation/ios/bluetooth_adapter.h" @class GNCMBlePeripheral, GNCMBleCentral; @@ -30,35 +34,163 @@ namespace location { namespace nearby { namespace ios { +/** InputStream that reads from GNCMConnection. */ +class BleInputStream : public InputStream { + public: + BleInputStream(); + ~BleInputStream() override; + + ExceptionOr<ByteArray> Read(std::int64_t size) override; + Exception Close() override; + + GNCMConnectionHandlers *GetConnectionHandlers() { return connectionHandlers_; } + + private: + GNCMConnectionHandlers *connectionHandlers_; + NSMutableArray<NSData *> *newDataPackets_; + NSMutableData *accumulatedData_; + NSCondition *condition_; +}; + +/** OutputStream that writes to GNCMConnection. */ +class BleOutputStream : public OutputStream { + public: + explicit BleOutputStream(id<GNCMConnection> connection) + : connection_(connection), condition_([[NSCondition alloc] init]) {} + ~BleOutputStream() override; + + Exception Write(const ByteArray &data) override; + Exception Flush() override; + Exception Close() override; + + private: + id<GNCMConnection> connection_; + NSCondition *condition_; +}; + +/** Concrete BleSocket implementation. */ +class BleSocket : public api::ble_v2::BleSocket { + public: + BleSocket(id<GNCMConnection> connection, BlePeripheral *peripheral); + ~BleSocket() override; + + InputStream &GetInputStream() override { return *input_stream_; } + OutputStream &GetOutputStream() override { return *output_stream_; } + Exception Close() override ABSL_LOCKS_EXCLUDED(mutex_); + BlePeripheral *GetRemotePeripheral() override { return peripheral_; } + + bool IsClosed() const ABSL_LOCKS_EXCLUDED(mutex_); + + private: + void DoClose() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + mutable absl::Mutex mutex_; + bool closed_ ABSL_GUARDED_BY(mutex_) = false; + std::unique_ptr<BleInputStream> input_stream_; + std::unique_ptr<BleOutputStream> output_stream_; + BlePeripheral *peripheral_; +}; + +/** Concrete BleServerSocket implementation. */ +class BleServerSocket : public api::ble_v2::BleServerSocket { + public: + ~BleServerSocket() override; + + std::unique_ptr<api::ble_v2::BleSocket> Accept() override ABSL_LOCKS_EXCLUDED(mutex_); + Exception Close() override ABSL_LOCKS_EXCLUDED(mutex_); + + bool Connect(std::unique_ptr<BleSocket> socket) ABSL_LOCKS_EXCLUDED(mutex_); + void SetCloseNotifier(std::function<void()> notifier) ABSL_LOCKS_EXCLUDED(mutex_); + + private: + Exception DoClose() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + mutable absl::Mutex mutex_; + absl::CondVar cond_; + absl::flat_hash_set<std::unique_ptr<BleSocket>> pending_sockets_ ABSL_GUARDED_BY(mutex_); + std::function<void()> close_notifier_ ABSL_GUARDED_BY(mutex_); + bool closed_ ABSL_GUARDED_BY(mutex_) = false; +}; + /** Concrete BleMedium implementation. */ class BleMedium : public api::ble_v2::BleMedium { public: - explicit BleMedium(api::BluetoothAdapter& adapter); + explicit BleMedium(api::BluetoothAdapter &adapter); // api::BleMedium: - bool StartAdvertising(const api::ble_v2::BleAdvertisementData& advertising_data, + bool StartAdvertising(const api::ble_v2::BleAdvertisementData &advertising_data, api::ble_v2::AdvertiseParameters advertise_set_parameters) override; bool StopAdvertising() override; - bool StartScanning(const Uuid& service_uuid, api::ble_v2::TxPowerLevel tx_power_level, + bool StartScanning(const Uuid &service_uuid, api::ble_v2::TxPowerLevel tx_power_level, api::ble_v2::BleMedium::ScanCallback scan_callback) override; bool StopScanning() override; std::unique_ptr<api::ble_v2::GattServer> StartGattServer( api::ble_v2::ServerGattConnectionCallback callback) override; std::unique_ptr<api::ble_v2::GattClient> ConnectToGattServer( - api::ble_v2::BlePeripheral& peripheral, api::ble_v2::TxPowerLevel tx_power_level, + api::ble_v2::BlePeripheral &peripheral, api::ble_v2::TxPowerLevel tx_power_level, api::ble_v2::ClientGattConnectionCallback callback) override; std::unique_ptr<api::ble_v2::BleServerSocket> OpenServerSocket( - const std::string& service_id) override; - std::unique_ptr<api::ble_v2::BleSocket> Connect(const std::string& service_id, + const std::string &service_id) override; + std::unique_ptr<api::ble_v2::BleSocket> Connect(const std::string &service_id, api::ble_v2::TxPowerLevel tx_power_level, - api::ble_v2::BlePeripheral& peripheral, - CancellationFlag* cancellation_flag) override; + api::ble_v2::BlePeripheral &peripheral, + CancellationFlag *cancellation_flag) override; bool IsExtendedAdvertisementsAvailable() override; private: - BluetoothAdapter* adapter_; - GNCMBlePeripheral* peripheral_; - GNCMBleCentral* central_; + // A concrete implemenation for GattServer. + class GattServer : public api::ble_v2::GattServer { + public: + GattServer() = default; + explicit GattServer(GNCMBlePeripheral *peripheral) : peripheral_(peripheral) {} + + absl::optional<api::ble_v2::GattCharacteristic> CreateCharacteristic( + const Uuid &service_uuid, const Uuid &characteristic_uuid, + const std::vector<api::ble_v2::GattCharacteristic::Permission> &permissions, + const std::vector<api::ble_v2::GattCharacteristic::Property> &properties) override; + + bool UpdateCharacteristic(const api::ble_v2::GattCharacteristic &characteristic, + const location::nearby::ByteArray &value) override; + void Stop() override; + + private: + GNCMBlePeripheral *peripheral_; + }; + + // A concrete implemenation for GattClient. + class GattClient : public api::ble_v2::GattClient { + public: + GattClient() = default; + explicit GattClient(GNCMBleCentral *central, const std::string &peripheral_id) + : central_(central), peripheral_id_(peripheral_id) {} + + bool DiscoverServiceAndCharacteristics(const Uuid &service_uuid, + const std::vector<Uuid> &characteristic_uuids) override; + + // NOLINTNEXTLINE + absl::optional<api::ble_v2::GattCharacteristic> GetCharacteristic( + const Uuid &service_uuid, const Uuid &characteristic_uuid) override; + + // NOLINTNEXTLINE + absl::optional<ByteArray> ReadCharacteristic( + const api::ble_v2::GattCharacteristic &characteristic) override; + + bool WriteCharacteristic(const api::ble_v2::GattCharacteristic &characteristic, + const ByteArray &value) override; + + void Disconnect() override; + + private: + GNCMBleCentral *central_; + std::string peripheral_id_; + absl::flat_hash_map<api::ble_v2::GattCharacteristic, ByteArray> gatt_characteristic_values_; + }; + + absl::Mutex mutex_; + BluetoothAdapter *adapter_; + GNCMBlePeripheral *peripheral_; + GNCMBleCentral *central_; + absl::flat_hash_map<std::string, BleServerSocket *> server_sockets_ ABSL_GUARDED_BY(mutex_); dispatch_queue_t callback_queue_; }; @@ -66,4 +198,5 @@ class BleMedium : public api::ble_v2::BleMedium { } // namespace nearby } // namespace location +#endif #endif // THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_IOS_BLE_H_ diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/ble.mm b/chromium/third_party/nearby/src/internal/platform/implementation/ios/ble.mm index 1c149138e8b..28116448f77 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/ble.mm +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/ble.mm @@ -15,7 +15,9 @@ #import "internal/platform/implementation/ios/ble.h" #include <CoreBluetooth/CoreBluetooth.h> +#include <functional> #include <string> +#include <utility> #include "internal/platform/implementation/ble_v2.h" #import "internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.h" @@ -27,10 +29,288 @@ namespace location { namespace nearby { namespace ios { +namespace { + +CBAttributePermissions PermissionToCBPermissions( + const std::vector<api::ble_v2::GattCharacteristic::Permission>& permissions) { + CBAttributePermissions characteristPermissions = 0; + for (const auto& permission : permissions) { + switch (permission) { + case api::ble_v2::GattCharacteristic::Permission::kRead: + characteristPermissions |= CBAttributePermissionsReadable; + break; + case api::ble_v2::GattCharacteristic::Permission::kWrite: + characteristPermissions |= CBAttributePermissionsWriteable; + break; + case api::ble_v2::GattCharacteristic::Permission::kLast: + case api::ble_v2::GattCharacteristic::Permission::kUnknown: + default:; // fall through + } + } + return characteristPermissions; +} + +CBCharacteristicProperties PropertiesToCBProperties( + const std::vector<api::ble_v2::GattCharacteristic::Property>& properties) { + CBCharacteristicProperties characteristicProperties = 0; + for (const auto& property : properties) { + switch (property) { + case api::ble_v2::GattCharacteristic::Property::kRead: + characteristicProperties |= CBCharacteristicPropertyRead; + break; + case api::ble_v2::GattCharacteristic::Property::kWrite: + characteristicProperties |= CBCharacteristicPropertyWrite; + break; + case api::ble_v2::GattCharacteristic::Property::kIndicate: + characteristicProperties |= CBCharacteristicPropertyIndicate; + break; + case api::ble_v2::GattCharacteristic::Property::kLast: + case api::ble_v2::GattCharacteristic::Property::kUnknown: + default:; // fall through + } + } + return characteristicProperties; +} + +} // namespace + using ::location::nearby::api::ble_v2::BleAdvertisementData; using ::location::nearby::api::ble_v2::TxPowerLevel; using ScanCallback = ::location::nearby::api::ble_v2::BleMedium::ScanCallback; +/** InputStream that reads from GNCMConnection. */ +BleInputStream::BleInputStream() + : newDataPackets_([NSMutableArray array]), + accumulatedData_([NSMutableData data]), + condition_([[NSCondition alloc] init]) { + // Create the handlers of incoming data from the remote endpoint. + connectionHandlers_ = [GNCMConnectionHandlers + payloadHandler:^(NSData* data) { + [condition_ lock]; + // Add the incoming data to the data packet array to be processed in read() below. + [newDataPackets_ addObject:data]; + [condition_ signal]; + [condition_ unlock]; + } + disconnectedHandler:^{ + [condition_ lock]; + // Release the data packet array, meaning the stream has been closed or severed. + newDataPackets_ = nil; + [condition_ signal]; + [condition_ unlock]; + }]; +} + +BleInputStream::~BleInputStream() { + NSCAssert(!newDataPackets_, @"BleInputStream not closed before destruction"); +} + +ExceptionOr<ByteArray> BleInputStream::Read(std::int64_t size) { + // Block until either (a) the connection has been closed, (b) we have enough data to return. + NSData* dataToReturn; + [condition_ lock]; + while (true) { + // Check if the stream has been closed or severed. + if (!newDataPackets_) break; + + if (newDataPackets_.count > 0) { + // Add the packet data to the accumulated data. + for (NSData* data in newDataPackets_) { + if (data.length > 0) { + [accumulatedData_ appendData:data]; + } + } + [newDataPackets_ removeAllObjects]; + } + + if ((size == -1) && (accumulatedData_.length > 0)) { + // Return all of the data. + dataToReturn = accumulatedData_; + accumulatedData_ = [NSMutableData data]; + break; + } else if (accumulatedData_.length > 0) { + // Return up to |size| bytes of the data. + std::int64_t sizeToReturn = (accumulatedData_.length < size) ? accumulatedData_.length : size; + NSRange range = NSMakeRange(0, (NSUInteger)sizeToReturn); + dataToReturn = [accumulatedData_ subdataWithRange:range]; + [accumulatedData_ replaceBytesInRange:range withBytes:nil length:0]; + break; + } + + [condition_ wait]; + } + [condition_ unlock]; + + if (dataToReturn) { + NSLog(@"[NEARBY] Input stream: Received data of size: %lu", (unsigned long)dataToReturn.length); + return ExceptionOr<ByteArray>(ByteArrayFromNSData(dataToReturn)); + } else { + return ExceptionOr<ByteArray>{Exception::kIo}; + } +} + +Exception BleInputStream::Close() { + // Unblock pending read operation. + [condition_ lock]; + newDataPackets_ = nil; + [condition_ signal]; + [condition_ unlock]; + return {Exception::kSuccess}; +} + +/** OutputStream that writes to GNCMConnection. */ +BleOutputStream::~BleOutputStream() { + NSCAssert(!connection_, @"BleOutputStream not closed before destruction"); +} + +Exception BleOutputStream::Write(const ByteArray& data) { + [condition_ lock]; + NSLog(@"[NEARBY] Sending data of size: %lu", (unsigned long)NSDataFromByteArray(data).length); + + NSMutableData* packet = [NSMutableData dataWithData:NSDataFromByteArray(data)]; + + // Send the data, blocking until the completion handler is called. + __block GNCMPayloadResult sendResult = GNCMPayloadResultFailure; + __block bool isComplete = NO; + NSCondition* condition = condition_; // don't capture |this| in completion + + // Check if connection_ is nil, then just don't wait and return as failure. + if (connection_ != nil) { + [connection_ sendData:packet + progressHandler:^(size_t count) { + } + completion:^(GNCMPayloadResult result) { + // Make sure we haven't already reported completion before. This prevents a crash + // where we try leaving a dispatch group more times than we entered it. + // b/79095653. + if (isComplete) { + return; + } + isComplete = YES; + sendResult = result; + [condition lock]; + [condition signal]; + [condition unlock]; + }]; + [condition_ wait]; + [condition_ unlock]; + } else { + sendResult = GNCMPayloadResultFailure; + [condition_ unlock]; + } + + if (sendResult == GNCMPayloadResultSuccess) { + return {Exception::kSuccess}; + } else { + return {Exception::kIo}; + } +} + +Exception BleOutputStream::Flush() { + // The write() function blocks until the data is received by the remote endpoint, so there's + // nothing to do here. + return {Exception::kSuccess}; +} + +Exception BleOutputStream::Close() { + // Unblock pending write operation. + [condition_ lock]; + connection_ = nil; + [condition_ signal]; + [condition_ unlock]; + return {Exception::kSuccess}; +} + +/** BleSocket implementation.*/ +BleSocket::BleSocket(id<GNCMConnection> connection, BlePeripheral* peripheral) + : input_stream_(new BleInputStream()), + output_stream_(new BleOutputStream(connection)), + peripheral_(peripheral) {} + +BleSocket::~BleSocket() { + absl::MutexLock lock(&mutex_); + DoClose(); +} + +bool BleSocket::IsClosed() const { + absl::MutexLock lock(&mutex_); + return closed_; +} + +Exception BleSocket::Close() { + absl::MutexLock lock(&mutex_); + DoClose(); + return {Exception::kSuccess}; +} + +void BleSocket::DoClose() { + if (!closed_) { + input_stream_->Close(); + output_stream_->Close(); + closed_ = true; + } +} + +/** WifiLanServerSocket implementation. */ +BleServerSocket::~BleServerSocket() { + absl::MutexLock lock(&mutex_); + DoClose(); +} + +std::unique_ptr<api::ble_v2::BleSocket> BleServerSocket::Accept() { + absl::MutexLock lock(&mutex_); + while (!closed_ && pending_sockets_.empty()) { + cond_.Wait(&mutex_); + } + // Return early if closed. + if (closed_) return {}; + + auto remote_socket = std::move(pending_sockets_.extract(pending_sockets_.begin()).value()); + return std::move(remote_socket); +} + +bool BleServerSocket::Connect(std::unique_ptr<BleSocket> socket) { + absl::MutexLock lock(&mutex_); + if (closed_) { + return false; + } + // add client socket to the pending list + pending_sockets_.insert(std::move(socket)); + cond_.SignalAll(); + if (closed_) { + return false; + } + return true; +} + +void BleServerSocket::SetCloseNotifier(std::function<void()> notifier) { + absl::MutexLock lock(&mutex_); + close_notifier_ = std::move(notifier); +} + +Exception BleServerSocket::Close() { + absl::MutexLock lock(&mutex_); + return DoClose(); +} + +Exception BleServerSocket::DoClose() { + bool should_notify = !closed_; + closed_ = true; + if (should_notify) { + cond_.SignalAll(); + if (close_notifier_) { + auto notifier = std::move(close_notifier_); + mutex_.Unlock(); + // Notifier may contain calls to public API, and may cause deadlock, if + // mutex_ is held during the call. + notifier(); + mutex_.Lock(); + } + } + return {Exception::kSuccess}; +} + +/** BleMedium implementation. */ BleMedium::BleMedium(::location::nearby::api::BluetoothAdapter& adapter) : adapter_(static_cast<BluetoothAdapter*>(&adapter)) {} @@ -40,12 +320,36 @@ bool BleMedium::StartAdvertising( if (advertising_data.service_data.empty()) { return false; } - const std::string& service_uuid = advertising_data.service_data.begin()->first.Get16BitAsString(); + const auto& service_uuid = advertising_data.service_data.begin()->first.Get16BitAsString(); const ByteArray& service_data_bytes = advertising_data.service_data.begin()->second; - peripheral_ = - [[GNCMBlePeripheral alloc] initWithServiceUUID:ObjCStringFromCppString(service_uuid) - advertisementData:NSDataFromByteArray(service_data_bytes)]; + if (!peripheral_) { + peripheral_ = [[GNCMBlePeripheral alloc] init]; + } + + auto& peripheral = adapter_->GetPeripheral(); + [peripheral_ + startAdvertisingWithServiceUUID:ObjCStringFromCppString(service_uuid) + advertisementData:NSDataFromByteArray(service_data_bytes) + endpointConnectedHandler:^GNCMConnectionHandlers*(id<GNCMConnection> connection) { + // TODO(edwinwu): This server_socket is supposed to be gotten from the map by key of + // servcie_id. We now always get the first iteration since we don't know the key now. + // Try the way to move the Ble socket frame verification up to one layer. + std::string service_id; + BleServerSocket* server_socket; + if (!server_sockets_.empty()) { + service_id = server_sockets_.begin()->first; + server_socket = server_sockets_.begin()->second; + } else { + return nil; + } + auto socket = std::make_unique<BleSocket>(connection, &peripheral); + GNCMConnectionHandlers* connectionHandlers = + static_cast<BleInputStream&>(socket->GetInputStream()).GetConnectionHandlers(); + server_socket->Connect(std::move(socket)); + return connectionHandlers; + } + callbackQueue:callback_queue_]; return true; } @@ -56,11 +360,23 @@ bool BleMedium::StopAdvertising() { bool BleMedium::StartScanning(const Uuid& service_uuid, TxPowerLevel tx_power_level, ScanCallback scan_callback) { - central_ = [[GNCMBleCentral alloc] - initWithServiceUUID:ObjCStringFromCppString(service_uuid.Get16BitAsString()) - scanResultHandler:^(NSString* serviceUUID, NSData* serviceData){ - // TODO(b/228751356): Add scan callback implementation. - }]; + if (!central_) { + central_ = [[GNCMBleCentral alloc] init]; + } + + [central_ startScanningWithServiceUUID:ObjCStringFromCppString(service_uuid.Get16BitAsString()) + scanResultHandler:^(NSString* peripheralID, NSData* serviceData) { + BleAdvertisementData advertisement_data; + advertisement_data.service_data = {{service_uuid, ByteArrayFromNSData(serviceData)}}; + BlePeripheral& peripheral = adapter_->GetPeripheral(); + peripheral.SetPeripheralId(CppStringFromObjCString(peripheralID)); + scan_callback.advertisement_found_cb(peripheral, advertisement_data); + } + requestConnectionHandler:^(GNCMBleConnectionRequester connectionRequester) { + BlePeripheral& peripheral = adapter_->GetPeripheral(); + peripheral.SetConnectionRequester(connectionRequester); + } + callbackQueue:callback_queue_]; return true; } @@ -72,29 +388,196 @@ bool BleMedium::StopScanning() { std::unique_ptr<api::ble_v2::GattServer> BleMedium::StartGattServer( api::ble_v2::ServerGattConnectionCallback callback) { - return nullptr; + if (!peripheral_) { + peripheral_ = [[GNCMBlePeripheral alloc] init]; + } + return std::make_unique<GattServer>(peripheral_); } std::unique_ptr<api::ble_v2::GattClient> BleMedium::ConnectToGattServer( api::ble_v2::BlePeripheral& peripheral, TxPowerLevel tx_power_level, api::ble_v2::ClientGattConnectionCallback callback) { - return nullptr; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block NSError* connectedError; + BlePeripheral iosPeripheral = static_cast<BlePeripheral&>(peripheral); + std::string peripheral_id = iosPeripheral.GetPeripheralId(); + [central_ connectGattServerWithPeripheralID:ObjCStringFromCppString(peripheral_id) + gattConnectionResultHandler:^(NSError* _Nullable error) { + connectedError = error; + dispatch_semaphore_signal(semaphore); + }]; + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); + + if (connectedError) { + return nullptr; + } + return std::make_unique<GattClient>(central_, peripheral_id); } std::unique_ptr<api::ble_v2::BleServerSocket> BleMedium::OpenServerSocket( const std::string& service_id) { - return nullptr; + auto server_socket = std::make_unique<BleServerSocket>(); + server_socket->SetCloseNotifier([this, service_id]() { + absl::MutexLock lock(&mutex_); + server_sockets_.erase(service_id); + }); + absl::MutexLock lock(&mutex_); + server_sockets_.insert({service_id, server_socket.get()}); + return server_socket; } std::unique_ptr<api::ble_v2::BleSocket> BleMedium::Connect(const std::string& service_id, TxPowerLevel tx_power_level, api::ble_v2::BlePeripheral& peripheral, CancellationFlag* cancellation_flag) { - return nullptr; + NSString* serviceID = ObjCStringFromCppString(service_id); + __block std::unique_ptr<BleSocket> socket; + __block BlePeripheral ios_peripheral = static_cast<BlePeripheral&>(peripheral); + GNCMBleConnectionRequester connection_requester = ios_peripheral.GetConnectionRequester(); + if (!connection_requester) return {}; + + dispatch_group_t group = dispatch_group_create(); + dispatch_group_enter(group); + if (cancellation_flag->Cancelled()) { + NSLog(@"[NEARBY] BLE Connect: Has been cancelled: service_id=%@", serviceID); + dispatch_group_leave(group); // unblock + return {}; + } + + connection_requester(serviceID, ^(id<GNCMConnection> connection) { + // If the connection wasn't successfully established, return a NULL socket. + if (connection) { + socket = std::make_unique<BleSocket>(connection, &ios_peripheral); + } + + dispatch_group_leave(group); // unblock + return socket != nullptr + ? static_cast<BleInputStream&>(socket->GetInputStream()).GetConnectionHandlers() + : nullptr; + }); + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + + // Send the (empty) intro packet, which the BLE advertiser is expecting. + if (socket != nullptr) { + socket->GetOutputStream().Write(ByteArray()); + } + + return std::move(socket); } bool BleMedium::IsExtendedAdvertisementsAvailable() { return false; } +// NOLINTNEXTLINE +absl::optional<api::ble_v2::GattCharacteristic> BleMedium::GattServer::CreateCharacteristic( + const Uuid& service_uuid, const Uuid& characteristic_uuid, + const std::vector<api::ble_v2::GattCharacteristic::Permission>& permissions, + const std::vector<api::ble_v2::GattCharacteristic::Property>& properties) { + api::ble_v2::GattCharacteristic characteristic = {.uuid = characteristic_uuid, + .service_uuid = service_uuid, + .permissions = permissions, + .properties = properties}; + [peripheral_ + addCBServiceWithUUID:[CBUUID + UUIDWithString:ObjCStringFromCppString( + characteristic.service_uuid.Get16BitAsString())]]; + [peripheral_ + addCharacteristic:[[CBMutableCharacteristic alloc] + initWithType:[CBUUID UUIDWithString:ObjCStringFromCppString(std::string( + characteristic.uuid))] + properties:PropertiesToCBProperties(characteristic.properties) + value:nil + permissions:PermissionToCBPermissions(characteristic.permissions)]]; + return characteristic; +} + +bool BleMedium::GattServer::UpdateCharacteristic( + const api::ble_v2::GattCharacteristic& characteristic, + const location::nearby::ByteArray& value) { + [peripheral_ updateValue:NSDataFromByteArray(value) + forCharacteristic:[CBUUID UUIDWithString:ObjCStringFromCppString( + std::string(characteristic.uuid))]]; + return true; +} + +void BleMedium::GattServer::Stop() { [peripheral_ stopGATTService]; } + +bool BleMedium::GattClient::DiscoverServiceAndCharacteristics( + const Uuid& service_uuid, const std::vector<Uuid>& characteristic_uuids) { + // Discover all characteristics that may contain the advertisement. + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + gatt_characteristic_values_.clear(); + CBUUID* serviceUUID = [CBUUID UUIDWithString:ObjCStringFromCppString(std::string(service_uuid))]; + + absl::flat_hash_map<std::string, Uuid> gatt_characteristics; + NSMutableArray<CBUUID*>* characteristicUUIDs = + [NSMutableArray arrayWithCapacity:characteristic_uuids.size()]; + for (const auto& characteristic_uuid : characteristic_uuids) { + [characteristicUUIDs addObject:[CBUUID UUIDWithString:ObjCStringFromCppString( + std::string(characteristic_uuid))]]; + gatt_characteristics.insert({std::string(characteristic_uuid), characteristic_uuid}); + } + + [central_ discoverGattService:serviceUUID + gattCharacteristics:characteristicUUIDs + peripheralID:ObjCStringFromCppString(peripheral_id_) + gattDiscoverResultHandler:^(NSOrderedSet<CBCharacteristic*>* _Nullable cb_characteristics) { + if (cb_characteristics != nil) { + for (CBCharacteristic* cb_characteristic in cb_characteristics) { + Uuid characteristic_uuid; + auto const& it = gatt_characteristics.find( + CppStringFromObjCString(cb_characteristic.UUID.UUIDString)); + if (it == gatt_characteristics.end()) continue; + + api::ble_v2::GattCharacteristic characteristic = {.uuid = it->second, + .service_uuid = service_uuid}; + gatt_characteristic_values_.insert( + {characteristic, ByteArrayFromNSData(cb_characteristic.value)}); + } + } + + dispatch_semaphore_signal(semaphore); + }]; + + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); + + if (gatt_characteristic_values_.empty()) { + return false; + } + return true; +} + +// NOLINTNEXTLINE +absl::optional<api::ble_v2::GattCharacteristic> BleMedium::GattClient::GetCharacteristic( + const Uuid& service_uuid, const Uuid& characteristic_uuid) { + api::ble_v2::GattCharacteristic characteristic = {.uuid = characteristic_uuid, + .service_uuid = service_uuid}; + auto const it = gatt_characteristic_values_.find(characteristic); + if (it == gatt_characteristic_values_.end()) { + return absl::nullopt; // NOLINT + } + return it->first; +} + +// NOLINTNEXTLINE +absl::optional<ByteArray> BleMedium::GattClient::ReadCharacteristic( + const api::ble_v2::GattCharacteristic& characteristic) { + auto const it = gatt_characteristic_values_.find(characteristic); + if (it == gatt_characteristic_values_.end()) { + return absl::nullopt; // NOLINT + } + return it->second; +} + +bool BleMedium::GattClient::WriteCharacteristic( + const api::ble_v2::GattCharacteristic& characteristic, const ByteArray& value) { + // No op. + return false; +} + +void BleMedium::GattClient::Disconnect() { + [central_ disconnectGattServiceWithPeripheralID:ObjCStringFromCppString(peripheral_id_)]; +} + } // namespace ios } // namespace nearby } // namespace location diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/bluetooth_adapter.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/bluetooth_adapter.h index 82a08a8336b..c8f75d95cd3 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/bluetooth_adapter.h +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/bluetooth_adapter.h @@ -14,11 +14,13 @@ #ifndef THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_IOS_BLUETOOTH_ADAPTER_H_ #define THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_IOS_BLUETOOTH_ADAPTER_H_ +#ifdef __cplusplus #include <string> #include "internal/platform/implementation/ble_v2.h" #include "internal/platform/implementation/bluetooth_adapter.h" +#import "internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.h" namespace location { namespace nearby { @@ -31,6 +33,20 @@ class BlePeripheral : public api::ble_v2::BlePeripheral { public: std::string GetAddress() const override; + std::string GetPeripheralId() const { return peripheral_id_; } + + void SetPeripheralId(const std::string& peripheral_id) { + peripheral_id_ = peripheral_id; + } + + void SetConnectionRequester(GNCMBleConnectionRequester connection_requester) { + connection_requester_ = connection_requester; + } + + GNCMBleConnectionRequester GetConnectionRequester() { + return connection_requester_; + } + private: // Only BluetoothAdapter may instantiate BlePeripheral. friend class BluetoothAdapter; @@ -38,6 +54,8 @@ class BlePeripheral : public api::ble_v2::BlePeripheral { explicit BlePeripheral(BluetoothAdapter* adapter) : adapter_(*adapter) {} BluetoothAdapter& adapter_; + std::string peripheral_id_; + GNCMBleConnectionRequester connection_requester_; }; // Concrete BluetoothAdapter implementation. @@ -56,7 +74,10 @@ class BluetoothAdapter : public api::BluetoothAdapter { ScanMode GetScanMode() const override { return mode_; } bool SetScanMode(ScanMode mode) override { return false; } std::string GetName() const override { return name_; } - bool SetName(absl::string_view name) override { + bool SetName(absl::string_view name) { + return SetName(name, /* persist= */ true); + } + bool SetName(absl::string_view name, bool persist) override { name_ = std::string(name); return true; } @@ -79,4 +100,5 @@ class BluetoothAdapter : public api::BluetoothAdapter { } // namespace nearby } // namespace location +#endif #endif // THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_IOS_BLUETOOTH_ADAPTER_H_ diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/log_message.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/log_message.h index 0c14c7d83ab..e83b45dae42 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/log_message.h +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/log_message.h @@ -14,6 +14,7 @@ #ifndef PLATFORM_IMPL_IOS_LOG_MESSAGE_H_ #define PLATFORM_IMPL_IOS_LOG_MESSAGE_H_ +#ifdef __cplusplus #ifdef NEARBY_SWIFTPM #include <sstream> @@ -52,4 +53,5 @@ class LogMessage : public api::LogMessage { } // namespace nearby } // namespace location +#endif #endif // IPHONE_SHARED_NEARBY_CONNECTIONS_SOURCE_PLATFORM_LOG_MESSAGE_H_ diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/multi_thread_executor.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/multi_thread_executor.h index cd8372de5d3..485b77ad01a 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/multi_thread_executor.h +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/multi_thread_executor.h @@ -14,6 +14,7 @@ #ifndef PLATFORM_IMPL_IOS_MULTI_THREAD_EXECUTOR_H_ #define PLATFORM_IMPL_IOS_MULTI_THREAD_EXECUTOR_H_ +#ifdef __cplusplus #import "internal/platform/implementation/ios/scheduled_executor.h" #include "internal/platform/implementation/submittable_executor.h" @@ -44,4 +45,5 @@ class MultiThreadExecutor : public api::SubmittableExecutor { } // namespace nearby } // namespace location +#endif #endif // PLATFORM_IMPL_IOS_MULTI_THREAD_EXECUTOR_H_ diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/scheduled_executor.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/scheduled_executor.h index 580389997c4..104c0504469 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/scheduled_executor.h +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/scheduled_executor.h @@ -14,6 +14,7 @@ #ifndef PLATFORM_IMPL_IOS_SCHEDULED_EXECUTOR_H_ #define PLATFORM_IMPL_IOS_SCHEDULED_EXECUTOR_H_ +#ifdef __cplusplus #import <Foundation/Foundation.h> @@ -65,4 +66,5 @@ class ScheduledExecutor : public api::ScheduledExecutor { } // namespace nearby } // namespace location +#endif #endif // PLATFORM_IMPL_IOS_SCHEDULED_EXECUTOR_H_ diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/single_thread_executor.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/single_thread_executor.h index 28d55fa18b8..3029c8bbf79 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/single_thread_executor.h +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/single_thread_executor.h @@ -14,6 +14,7 @@ #ifndef PLATFORM_IMPL_IOS_SINGLE_THREAD_EXECUTOR_H_ #define PLATFORM_IMPL_IOS_SINGLE_THREAD_EXECUTOR_H_ +#ifdef __cplusplus #import "internal/platform/implementation/ios/multi_thread_executor.h" @@ -31,4 +32,5 @@ class SingleThreadExecutor : public MultiThreadExecutor { } // namespace nearby } // namespace location +#endif #endif // PLATFORM_IMPL_IOS_SINGLE_THREAD_EXECUTOR_H_ diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/utils.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/utils.h index fd864a13f80..3f4a0c8caf0 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/utils.h +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/utils.h @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#ifdef __cplusplus + #import <CoreBluetooth/CoreBluetooth.h> #import <Foundation/Foundation.h> @@ -72,3 +74,5 @@ absl::flat_hash_map<std::string, std::string> AbslHashMapFromObjCTxtRecords( } // namespace location NS_ASSUME_NONNULL_END + +#endif diff --git a/chromium/third_party/nearby/src/internal/platform/implementation/ios/wifi_lan.h b/chromium/third_party/nearby/src/internal/platform/implementation/ios/wifi_lan.h index d616fe202d4..914dfc1b29f 100644 --- a/chromium/third_party/nearby/src/internal/platform/implementation/ios/wifi_lan.h +++ b/chromium/third_party/nearby/src/internal/platform/implementation/ios/wifi_lan.h @@ -14,6 +14,7 @@ #ifndef PLATFORM_IMPL_IOS_WIFI_LAN_H_ #define PLATFORM_IMPL_IOS_WIFI_LAN_H_ +#ifdef __cplusplus #import <Foundation/Foundation.h> #include <string> @@ -139,6 +140,9 @@ class WifiLanMedium : public api::WifiLanMedium { WifiLanMedium(const WifiLanMedium&) = delete; WifiLanMedium& operator=(const WifiLanMedium&) = delete; + // Check if a network connection to a primary router exist. + bool IsNetworkConnected() const override { return true; } + // api::WifiLanMedium: bool StartAdvertising(const NsdServiceInfo& nsd_service_info) override ABSL_LOCKS_EXCLUDED(mutex_); @@ -199,4 +203,5 @@ class WifiLanMedium : public api::WifiLanMedium { } // namespace nearby } // namespace location +#endif #endif // PLATFORM_IMPL_IOS_WIFI_LAN_H_ |