From 9ce81d853447ab28ff675aa315d7cbb8fc1bbd19 Mon Sep 17 00:00:00 2001 From: edwinwu Date: Fri, 24 Jun 2022 15:56:17 -0700 Subject: [PATCH] [BLE Refactor] Implement iOS BLE incoming connection. PiperOrigin-RevId: 457110588 --- connections/implementation/mediums/ble_v2.cc | 5 +- .../platform/implementation/ios/Mediums/BUILD | 18 +-- .../ios/Mediums/Ble/GNCMBleConnection.h | 12 +- .../ios/Mediums/Ble/GNCMBleConnection.m | 61 +++++++- .../ios/Mediums/Ble/GNCMBlePeripheral.h | 9 +- .../ios/Mediums/Ble/GNCMBlePeripheral.m | 106 ++++++++++++- .../ios/Mediums/Ble/GNCMBleUtils.h | 20 ++- .../ios/Mediums/Ble/GNCMBleUtils.m | 83 ---------- .../ios/Mediums/Ble/GNCMBleUtils.mm | 143 ++++++++++++++++++ internal/platform/implementation/ios/ble.h | 7 +- internal/platform/implementation/ios/ble.mm | 45 ++++-- 11 files changed, 373 insertions(+), 136 deletions(-) delete mode 100644 internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.m create mode 100644 internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.mm diff --git a/connections/implementation/mediums/ble_v2.cc b/connections/implementation/mediums/ble_v2.cc index d0e22137..884a0150 100644 --- a/connections/implementation/mediums/ble_v2.cc +++ b/connections/implementation/mediums/ble_v2.cc @@ -395,8 +395,9 @@ bool BleV2::StartAcceptingConnections(const std::string& service_id, // listening for new incoming connections until StopAcceptingConnections() is // invoked. accept_loops_runner_.Execute( - "ble-accept", [this, &service_id, callback = std::move(callback), - server_socket = std::move(owned_server_socket)]() mutable { + "ble-accept", + [this, service_id = service_id, callback = std::move(callback), + server_socket = std::move(owned_server_socket)]() mutable { while (true) { BleV2Socket client_socket = server_socket.Accept(); if (!client_socket.IsValid()) { diff --git a/internal/platform/implementation/ios/Mediums/BUILD b/internal/platform/implementation/ios/Mediums/BUILD index 8519ef27..f6f91c1c 100644 --- a/internal/platform/implementation/ios/Mediums/BUILD +++ b/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"); @@ -23,7 +21,7 @@ objc_library( "Ble/GNCMBleCentral.m", "Ble/GNCMBleConnection.m", "Ble/GNCMBlePeripheral.m", - "Ble/GNCMBleUtils.m", + "Ble/GNCMBleUtils.mm", "GNCLeaks.m", "GNCMConnection.m", "WifiLan/GNCMBonjourBrowser.m", @@ -44,24 +42,12 @@ objc_library( "WifiLan/GNCMBonjourUtils.h", ], deps = [ - ":ObjCProtos", "//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/internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.h b/internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.h index 6665862a..8897a6d7 100644 --- a/internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.h +++ b/internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.h @@ -25,8 +25,18 @@ NS_ASSUME_NONNULL_BEGIN @interface GNCMBleConnection : NSObject @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:(NSString *)serviceId + serviceID:(nullable NSString *)serviceID + expectedIntroPacket:(BOOL)expectedIntroPacket callbackQueue:(dispatch_queue_t)callbackQueue; @end diff --git a/internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.m b/internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.m index 637b5f2d..acb950ed 100644 --- a/internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.m +++ b/internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.m @@ -25,21 +25,25 @@ NS_ASSUME_NONNULL_BEGIN @interface GNCMBleConnection () @property(nonatomic) dispatch_queue_t selfQueue; @property(nonatomic) GNSSocket *socket; -@property(nonatomic) NSData *serviceIdHash; +@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:(NSString *)serviceId + 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 = GNCMServiceIdHash(serviceId); + connection.serviceIDHash = serviceID ? GNCMServiceIDHash(serviceID) : nil; connection.callbackQueue = callbackQueue; connection.selfQueue = dispatch_queue_create("GNCMBleConnectionQueue", DISPATCH_QUEUE_SERIAL); + connection.expectedIntroPacket = expectedIntroPacket; return connection; } @@ -49,16 +53,26 @@ NS_ASSUME_NONNULL_BEGIN } #pragma mark GNCMConnection - - (void)sendData:(NSData *)data progressHandler:(GNCMProgressHandler)progressHandler completion:(GNCMPayloadResultHandler)completion { dispatch_async(_selfQueue, ^{ - [_socket sendData:data + 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 * data.length)); + progressHandler((size_t)(progress * packet.length)); }); } completion:^(NSError *error) { @@ -86,10 +100,43 @@ NS_ASSUME_NONNULL_BEGIN } - (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(data); + _connectionHandlers.payloadHandler(packet); }); } }); diff --git a/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h b/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h index 5eef0133..87361cb5 100644 --- a/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h +++ b/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h @@ -14,6 +14,8 @@ #import +#import "internal/platform/implementation/ios/Mediums/GNCMConnection.h" + @class CBCharacteristic; @class CBUUID; @@ -63,9 +65,14 @@ NS_ASSUME_NONNULL_BEGIN * * @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. */ - (BOOL)startAdvertisingWithServiceUUID:(NSString *)serviceUUID - advertisementData:(NSData *)advertisementData; + advertisementData:(NSData *)advertisementData + endpointConnectedHandler:(GNCMConnectionHandler)endpointConnectedHandler + callbackQueue:(nullable dispatch_queue_t)callbackQueue; @end diff --git a/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.m b/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.m index 14e79461..3961f7fe 100644 --- a/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.m +++ b/internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.m @@ -17,6 +17,11 @@ #import #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 @@ -45,6 +50,16 @@ typedef NS_ENUM(NSUInteger, GNCMPeripheralState) { 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)init { @@ -53,11 +68,6 @@ typedef NS_ENUM(NSUInteger, GNCMPeripheralState) { // 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; @@ -69,6 +79,8 @@ typedef NS_ENUM(NSUInteger, GNCMPeripheralState) { // must never be captured by any escaping block used in this class. dispatch_sync(_selfQueue, ^{ [self stopAdvertisingInternal]; + + _callbacksEnabled = NO; }); } @@ -104,12 +116,55 @@ typedef NS_ENUM(NSUInteger, GNCMPeripheralState) { } - (BOOL)startAdvertisingWithServiceUUID:(NSString *)serviceUUID - advertisementData:(NSData *)advertisementData { + 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; @@ -126,6 +181,7 @@ typedef NS_ENUM(NSUInteger, GNCMPeripheralState) { if (peripheral.state == CBManagerStatePoweredOn && !peripheral.isAdvertising && _state == GNCMPeripheralStateAdvertising) { NSLog(@"[NEARBY] CBPeripheralManager powered on; starting advertising"); + [_socketPeripheralManager start]; [self startAdvertisingInternal]; } else { NSLog(@"[NEARBY] CBPeripheralManager not powered on; stopping advertising"); @@ -149,7 +205,7 @@ typedef NS_ENUM(NSUInteger, GNCMPeripheralState) { - (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request { - NSLog(@"[NEARBY] peripheralManager:didReceiveReadRequest"); + 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]; @@ -196,6 +252,42 @@ typedef NS_ENUM(NSUInteger, GNCMPeripheralState) { } } +/** + * 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 NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.h b/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.h index af2b5f35..fd85edba 100644 --- a/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.h +++ b/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.h @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#import +#import NS_ASSUME_NONNULL_BEGIN @@ -27,11 +27,23 @@ extern "C" { /** 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, + GNCMBleAdvertisementLengthServiceIDHash = 3, }; -/** Computes a hash from a service ID string. It is used in the NC BLE advertisement. */ -NSData *GNCMServiceIdHash(NSString *serviceId); +/** 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 diff --git a/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.m b/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.m deleted file mode 100644 index e31eac3e..00000000 --- a/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.m +++ /dev/null @@ -1,83 +0,0 @@ -// 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" - -#import "internal/platform/implementation/ios/GNCUtils.h" -#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.h" -#import "GoogleToolboxForMac/GTMLogger.h" - -NS_ASSUME_NONNULL_BEGIN - -static const NSTimeInterval kBleSocketConnectionTimeout = 5.0; - -NSData *GNCMServiceIdHash(NSString *serviceId) { - return [GNCSha256String(serviceId) - subdataWithRange:NSMakeRange(0, GNCMBleAdvertisementLengthServiceIdHash)]; -} - -@interface GNCMBleSocketDelegate : NSObject -@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/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.mm b/internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.mm new file mode 100644 index 00000000..c24fa656 --- /dev/null +++ b/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 +#include + +#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 +@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/internal/platform/implementation/ios/ble.h b/internal/platform/implementation/ios/ble.h index 3aa2a346..856d0794 100644 --- a/internal/platform/implementation/ios/ble.h +++ b/internal/platform/implementation/ios/ble.h @@ -70,14 +70,13 @@ class BleOutputStream : public OutputStream { /** Concrete BleSocket implementation. */ class BleSocket : public api::ble_v2::BleSocket { public: - BleSocket() = default; - explicit BleSocket(id connection); + BleSocket(id 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 nullptr; } + BlePeripheral *GetRemotePeripheral() override { return peripheral_; } bool IsClosed() const ABSL_LOCKS_EXCLUDED(mutex_); @@ -88,6 +87,7 @@ class BleSocket : public api::ble_v2::BleSocket { bool closed_ ABSL_GUARDED_BY(mutex_) = false; std::unique_ptr input_stream_; std::unique_ptr output_stream_; + BlePeripheral *peripheral_; }; /** Concrete BleServerSocket implementation. */ @@ -192,6 +192,7 @@ class BleMedium : public api::ble_v2::BleMedium { absl::flat_hash_map server_sockets_ ABSL_GUARDED_BY(mutex_); absl::flat_hash_map connection_requesters_ ABSL_GUARDED_BY(mutex_); + dispatch_queue_t callback_queue_; }; } // namespace ios diff --git a/internal/platform/implementation/ios/ble.mm b/internal/platform/implementation/ios/ble.mm index 96d3aafa..13591eb7 100644 --- a/internal/platform/implementation/ios/ble.mm +++ b/internal/platform/implementation/ios/ble.mm @@ -24,7 +24,6 @@ #import "internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h" #include "internal/platform/implementation/ios/bluetooth_adapter.h" #include "internal/platform/implementation/ios/utils.h" -#import "GoogleToolboxForMac/GTMLogger.h" namespace location { namespace nearby { @@ -143,8 +142,7 @@ ExceptionOr BleInputStream::Read(std::int64_t size) { [condition_ unlock]; if (dataToReturn) { - GTMLoggerInfo(@"[NEARBY] Input stream: Received data of size: %lu", - (unsigned long)dataToReturn.length); + NSLog(@"[NEARBY] Input stream: Received data of size: %lu", (unsigned long)dataToReturn.length); return ExceptionOr(ByteArrayFromNSData(dataToReturn)); } else { return ExceptionOr{Exception::kIo}; @@ -167,8 +165,7 @@ BleOutputStream::~BleOutputStream() { Exception BleOutputStream::Write(const ByteArray& data) { [condition_ lock]; - GTMLoggerInfo(@"[NEARBY] Sending data of size: %lu", - (unsigned long)NSDataFromByteArray(data).length); + NSLog(@"[NEARBY] Sending data of size: %lu", (unsigned long)NSDataFromByteArray(data).length); NSMutableData* packet = [NSMutableData dataWithData:NSDataFromByteArray(data)]; @@ -225,8 +222,10 @@ Exception BleOutputStream::Close() { } /** BleSocket implementation.*/ -BleSocket::BleSocket(id connection) - : input_stream_(new BleInputStream()), output_stream_(new BleOutputStream(connection)) {} +BleSocket::BleSocket(id connection, BlePeripheral* peripheral) + : input_stream_(new BleInputStream()), + output_stream_(new BleOutputStream(connection)), + peripheral_(peripheral) {} BleSocket::~BleSocket() { absl::MutexLock lock(&mutex_); @@ -328,8 +327,29 @@ bool BleMedium::StartAdvertising( peripheral_ = [[GNCMBlePeripheral alloc] init]; } - [peripheral_ startAdvertisingWithServiceUUID:ObjCStringFromCppString(service_uuid) - advertisementData:NSDataFromByteArray(service_data_bytes)]; + auto& peripheral = adapter_->GetPeripheral(); + [peripheral_ + startAdvertisingWithServiceUUID:ObjCStringFromCppString(service_uuid) + advertisementData:NSDataFromByteArray(service_data_bytes) + endpointConnectedHandler:^GNCMConnectionHandlers*(id 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(connection, &peripheral); + GNCMConnectionHandlers* connectionHandlers = + static_cast(socket->GetInputStream()).GetConnectionHandlers(); + server_socket->Connect(std::move(socket)); + return connectionHandlers; + } + callbackQueue:callback_queue_]; return true; } @@ -419,10 +439,11 @@ std::unique_ptr BleMedium::Connect(const std::string& se dispatch_group_t group = dispatch_group_create(); dispatch_group_enter(group); __block std::unique_ptr socket; + __block BlePeripheral ios_peripheral = static_cast(peripheral); if (connection_requester != nil) { if (cancellation_flag->Cancelled()) { - GTMLoggerError(@"[NEARBY] BLE Connect: Has been cancelled: service_id=%@", - ObjCStringFromCppString(service_id)); + NSLog(@"[NEARBY] BLE Connect: Has been cancelled: service_id=%@", + ObjCStringFromCppString(service_id)); dispatch_group_leave(group); // unblock return {}; } @@ -430,7 +451,7 @@ std::unique_ptr BleMedium::Connect(const std::string& se connection_requester(^(id connection) { // If the connection wasn't successfully established, return a NULL socket. if (connection) { - socket = std::make_unique(connection); + socket = std::make_unique(connection, &ios_peripheral); } dispatch_group_leave(group); // unblock