summaryrefslogtreecommitdiff
path: root/chromium/third_party/nearby/src/internal/platform/implementation/ios
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/third_party/nearby/src/internal/platform/implementation/ios')
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/BUILD10
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/BUILD25
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.h77
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.m466
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.h43
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.m147
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h47
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.m238
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.h58
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.mm143
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/BUILD87
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager+Private.h38
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.h201
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.m372
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager+Private.h73
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.h99
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.m766
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/GNSCentral.h17
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/GNSPeripheral.h17
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/GNSShared.h16
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager+Private.h70
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager.h111
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager.m546
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager+Private.h132
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager.h81
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager.m551
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h144
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.h155
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.m238
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils+Private.h47
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.h47
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.m102
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSWeavePacket.h242
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSWeavePacket.m416
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Central/GNSCentralManagerTest.m299
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Central/GNSCentralPeerManagerTest.m465
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Peripheral/GNSPeripheralManagerTest.m836
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Peripheral/GNSPeripheralServiceManagerTest.m671
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Shared/GNSSocketTest.m355
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Mediums/Ble/Sockets/Tests/Shared/GNSWeavePacketTest.m310
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Tests/GNCBLEATest.mm22
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Tests/GNCBleTest.mm41
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/Tests/GNCMultiThreadExecutorTest.mm8
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/ble.h155
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/ble.mm509
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/bluetooth_adapter.h24
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/log_message.h2
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/multi_thread_executor.h2
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/scheduled_executor.h2
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/single_thread_executor.h2
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/utils.h4
-rw-r--r--chromium/third_party/nearby/src/internal/platform/implementation/ios/wifi_lan.h5
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_