// Copyright 2023 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/apple/Mediums/BLE/GNCBLEMedium.h" #import #import #import "internal/platform/implementation/apple/Flags/GNCFeatureFlags.h" #import "internal/platform/implementation/apple/Log/GNCLogger.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEError.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEGATTClient.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEGATTServer.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEL2CAPClient.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEL2CAPServer.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCCentralManager.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCPeripheral.h" #import "internal/platform/implementation/apple/Mediums/BLE/NSData+GNCBase85.h" #import "internal/platform/implementation/apple/Mediums/BLE/NSData+GNCWebSafeBase64.h" NS_ASSUME_NONNULL_BEGIN static char *const kBLEMediumQueueLabel = "com.nearby.GNCBLEMedium"; // TODO: b/762454867 - Make this a flag. static const NSTimeInterval kPeripheralConnectTimeout = 10.0; // 10 seconds timeout static const NSTimeInterval kStopTimeout = 5.0; static NSError *AlreadyScanningError() { return [NSError errorWithDomain:GNCBLEErrorDomain code:GNCBLEErrorAlreadyScanning userInfo:nil]; } static GNCBLEL2CAPServer *_Nonnull CreateL2CapServer( id _Nullable peripheralManager) { if (!peripheralManager) { return [[GNCBLEL2CAPServer alloc] init]; } else { return [[GNCBLEL2CAPServer alloc] initWithPeripheralManager:peripheralManager queue:dispatch_get_main_queue()]; } } @interface GNCBLEMedium () @end @implementation GNCBLEMedium { dispatch_queue_t _queue; id _centralManager; // The active GATT server, or @nil if one hasn't been started yet. GNCBLEGATTServer *_server; // The active L2CAP server, or @nil if one hasn't been started yet. GNCBLEL2CAPServer *_l2capServer; // The active L2CAP client, or @nil if one hasn't been started yet. GNCBLEL2CAPClient *_l2capClient; // The PSM number of the remote peripheral's L2CAP server. uint16_t _l2capPSM; // The services that is being actively scanned for. NSMutableArray *_scanningServiceUUIDs; // The handler called when an advertisement for the service represented by @c _serviceUUID has // been discovered. This will be called continuously, until the peripheral disappears. GNCAdvertisementFoundHandler _advertisementFoundHandler; // A peripheral to connection completion handler map. Used to track connection attempts. When a // connection attempt has succeeded or failed, the completion handler is called and removed from // the map. NSMutableDictionary *_gattConnectionCompletionHandlers; // A peripheral to disconnection handler map. Used to track when a peripheral becomes // disconnected. Once disconnected, the disconnection handler is called and removed from the map. NSMutableDictionary *_gattDisconnectionHandlers; // A peripheral to L2CAP stream completion handler map. Used to track L2CAP stream attempts. When // a L2CAP stream attempt has succeeded or failed, the completion handler is called and removed // from the map. NSMutableDictionary *_l2capStreamCompletionHandlers; // The block to call when the BLE connection times out. dispatch_block_t _connectionTimeoutBlock; } - (instancetype)init { dispatch_queue_t queue = dispatch_queue_create(kBLEMediumQueueLabel, DISPATCH_QUEUE_SERIAL); CBCentralManager *centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:queue options:@{CBCentralManagerOptionShowPowerAlertKey : @NO}]; return [self initWithCentralManager:centralManager queue:queue]; } // This is private and should only be used for tests. The provided central manager must call // delegate methods on the main queue. - (instancetype)initWithCentralManager:(id)centralManager queue:(nullable dispatch_queue_t)queue { self = [super init]; if (self) { _queue = queue ?: dispatch_get_main_queue(); _centralManager = centralManager; _centralManager.centralDelegate = self; _gattConnectionCompletionHandlers = [NSMutableDictionary dictionary]; _gattDisconnectionHandlers = [NSMutableDictionary dictionary]; _scanningServiceUUIDs = [NSMutableArray array]; _l2capStreamCompletionHandlers = [NSMutableDictionary dictionary]; _l2capPSM = 0; } return self; } - (void)stop { dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); dispatch_async(_queue, ^{ [self cancelConnectionTimeout]; dispatch_semaphore_signal(semaphore); }); dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, kStopTimeout * NSEC_PER_SEC); // 5 seconds timeout if (dispatch_semaphore_wait(semaphore, timeout) != 0) { GNCLoggerError(@"GNCBLEMedium stop timeout."); } } - (BOOL)supportsExtendedAdvertisements { // TODO(b/294736083): CoreBluetooth doesn't support actually advertising any extensions, however // some devices can scan for them if the feature is available. If we return @c YES from this // method, we would be enabling advertising extensions (which won't work), so we must return @c NO // until we add support for a new method to check only if extended scans are supported. return NO; } - (void)startAdvertisingData:(NSDictionary *)serviceData completionHandler:(nullable GNCStartAdvertisingCompletionHandler)completionHandler { dispatch_async(_queue, ^{ if (!_server) { _server = [[GNCBLEGATTServer alloc] init]; } [_server startAdvertisingData:serviceData completionHandler:completionHandler]; }); } - (void)stopAdvertisingWithCompletionHandler: (nullable GNCStopAdvertisingCompletionHandler)completionHandler { dispatch_async(_queue, ^{ if (!_server) { if (completionHandler) { completionHandler(nil); } return; } [_server stopAdvertisingWithCompletionHandler:completionHandler]; }); } - (void)startScanningForService:(CBUUID *)serviceUUID advertisementFoundHandler:(GNCAdvertisementFoundHandler)advertisementFoundHandler completionHandler:(nullable GNCStartScanningCompletionHandler)completionHandler { [self startScanningForMultipleServices:@[ serviceUUID ] advertisementFoundHandler:advertisementFoundHandler completionHandler:completionHandler]; } - (void)startScanningForMultipleServices:(NSArray *)serviceUUIDs advertisementFoundHandler:(GNCAdvertisementFoundHandler)advertisementFoundHandler completionHandler: (nullable GNCStartScanningCompletionHandler)completionHandler { dispatch_async(_queue, ^{ if (_scanningServiceUUIDs.count > 0) { if (completionHandler) { completionHandler(AlreadyScanningError()); } return; } [_scanningServiceUUIDs addObjectsFromArray:serviceUUIDs]; _advertisementFoundHandler = advertisementFoundHandler; [self internalStartScanningIfPoweredOn]; if (completionHandler) { completionHandler(nil); } }); } - (void)stopScanningWithCompletionHandler: (nullable GNCStopScanningCompletionHandler)completionHandler { dispatch_async(_queue, ^{ [_scanningServiceUUIDs removeAllObjects]; _advertisementFoundHandler = nil; [_centralManager stopScan]; if (completionHandler) { completionHandler(nil); } }); } - (void)resumeMediumScanning:(nullable GNCStartScanningCompletionHandler)completionHandler { dispatch_async(_queue, ^{ [self internalStartScanningIfPoweredOn]; if (completionHandler) { completionHandler(nil); } }); } - (void)startGATTServerWithCompletionHandler: (nullable GNCGATTServerCompletionHandler)completionHandler { dispatch_async(_queue, ^{ if (!_server) { _server = [[GNCBLEGATTServer alloc] init]; } if (completionHandler) { completionHandler(_server, nil); } }); } - (void)connectToGATTServerForPeripheral:(id)remotePeripheral disconnectionHandler:(nullable GNCGATTDisconnectionHandler)disconnectionHandler completionHandler: (nullable GNCGATTConnectionCompletionHandler)completionHandler { __weak __typeof__(self) weakSelf = self; dispatch_async(_queue, ^{ __strong __typeof__(self) strongSelf = weakSelf; if (!strongSelf) { GNCLoggerDebug(@"GNCBLEMedium instance no longer exists when connect to gatt server."); return; } strongSelf->_gattDisconnectionHandlers[remotePeripheral.identifier] = disconnectionHandler; strongSelf->_gattConnectionCompletionHandlers[remotePeripheral.identifier] = completionHandler; strongSelf->_l2capPSM = 0; if (GNCFeatureFlags.bleL2capEnabled) { [strongSelf connectToPeripheral:remotePeripheral withTimeout:kPeripheralConnectTimeout options:@{} timeoutCallback:^(NSError *error) { if (completionHandler) { completionHandler(nil, error); } }]; } else { [_centralManager connectPeripheral:remotePeripheral options:@{}]; } }); } - (void)openL2CAPServerWithPSMPublishedCompletionHandler: (GNCOpenL2CAPServerPSMPublishedCompletionHandler)psmPublishedCompletionHandler channelOpenedCompletionHandler: (GNCOpenL2CAPServerChannelOpendCompletionHandler) channelOpenedCompletionHandler peripheralManager: (nullable id)peripheralManager { dispatch_async(_queue, ^{ if (!_l2capServer) { _l2capServer = CreateL2CapServer(peripheralManager); } [_l2capServer startListeningChannelWithPSMPublishedCompletionHandler:psmPublishedCompletionHandler channelOpenedCompletionHandler:channelOpenedCompletionHandler]; }); } - (void)openL2CAPChannelWithPSM:(uint16_t)psm peripheral:(id)remotePeripheral completionHandler:(nullable GNCOpenL2CAPStreamCompletionHandler)completionHandler { __weak __typeof__(self) weakSelf = self; dispatch_async(_queue, ^{ __strong __typeof__(self) strongSelf = weakSelf; if (!strongSelf) { GNCLoggerDebug(@"GNCBLEMedium instance no longer exists when open L2CAP channel."); return; } if (!strongSelf->_l2capClient) { strongSelf->_l2capClient = [[GNCBLEL2CAPClient alloc] initWithRequestDisconnectionHandler:^(id _Nonnull peripheral) { dispatch_async(_queue, ^{ [_centralManager cancelPeripheralConnection:remotePeripheral]; }); }]; } strongSelf->_l2capStreamCompletionHandlers[remotePeripheral.identifier] = completionHandler; strongSelf->_l2capPSM = psm; // There is a Core Bluetooth problem: Either -didConnectPeripheral or // -didFailToConnectPeripheral should be called at this point, but sometimes neither is // called. // TODO: b/419127415 - Investigate the root cause of this problem. [strongSelf connectToPeripheral:remotePeripheral withTimeout:kPeripheralConnectTimeout options:@{} timeoutCallback:^(NSError *error) { if (completionHandler) { completionHandler(nil, error); } }]; }); } // This is private and should only be used for tests. The provided L2CAP client must call // delegate methods on the main queue. - (void)setL2CAPClient:(GNCBLEL2CAPClient *)l2capClient { _l2capClient = l2capClient; } #pragma mark - Internal - (void)internalStartScanningIfPoweredOn { dispatch_assert_queue(_queue); // Scanning can only be done when powered on and must be restarted if bluetooth is turned off // then back on. This will be called anytime the central manager's state changes, so // @c scanForPeripheralsWithServices:options: will be called anytime state transitions back to // powered on. if (_centralManager.state == CBManagerStatePoweredOn && _scanningServiceUUIDs.count > 0) { // Stop scanning just in case something outside of this class is already scanning. [_centralManager stopScan]; [_centralManager scanForPeripheralsWithServices:_scanningServiceUUIDs // Nearby relies on the existence of an advertisement for endpoint // discovery/lost events, so we must set this key to keep the stream // of duplicate delegate events flowing. This has adverse effect on // battery life, but currently necessary. options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES}]; } } - (NSDictionary *)decodeAdvertisementData: (NSDictionary *)advertisementData { dispatch_assert_queue(_queue); // If service data is available, return it directly. NSDictionary *serviceData = advertisementData[CBAdvertisementDataServiceDataKey]; if (serviceData) { return serviceData; } // Apple devices don't support advertising service data, so Apple devices advertise a base64 // encoded local name, while other devices advertise service data. Here we attempt to reconstruct // service data by decoding the local name. If successful, this is possibly a Nearby advertisement // on an Apple device. NSString *localName = advertisementData[CBAdvertisementDataLocalNameKey]; if (!localName) { return @{}; } NSData *data = GNCFeatureFlags.dctEnabled ? [[NSData alloc] initWithBase85EncodedString:localName] : [[NSData alloc] initWithWebSafeBase64EncodedString:localName]; // A Nearby Apple advertisement should only have a single service, so simply grab the first one if // it exists. NSArray *serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey]; CBUUID *serviceUUID = serviceUUIDs.firstObject; if (data && serviceUUID) { return @{serviceUUID : data}; } return @{}; } - (void)connectToPeripheral:(id)peripheral withTimeout:(NSTimeInterval)timeout options:(nullable NSDictionary *)options timeoutCallback:(void (^)(NSError *))callback { dispatch_assert_queue(_queue); [self cancelConnectionTimeout]; __weak __typeof__(self) weakSelf = self; _connectionTimeoutBlock = dispatch_block_create(0, ^{ __strong __typeof__(self) strongSelf = weakSelf; if (!strongSelf) { return; } strongSelf->_connectionTimeoutBlock = nil; // Perform timeout actions if (callback) { callback([NSError errorWithDomain:GNCBLEErrorDomain code:GNCBLEErrorBlePerperipheralConnectionTimeout userInfo:nil]); } }); if (_connectionTimeoutBlock) { dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); dispatch_after(delay, _queue, _connectionTimeoutBlock); } [_centralManager connectPeripheral:peripheral options:options]; } - (void)cancelConnectionTimeout { dispatch_assert_queue(_queue); if (_connectionTimeoutBlock) { dispatch_block_cancel(_connectionTimeoutBlock); _connectionTimeoutBlock = nil; } } - (void)internalOpenL2CAPChannel:(id)remotePeripheral { dispatch_assert_queue(_queue); __weak __typeof__(self) weakSelf = self; GNCOpenL2CAPStreamCompletionHandler handler = _l2capStreamCompletionHandlers[remotePeripheral.identifier]; _l2capStreamCompletionHandlers[remotePeripheral.identifier] = nil; if (!handler) { return; } [_l2capClient openL2CAPChannelWithPSM:_l2capPSM peripheral:remotePeripheral completionHandler:^(GNCBLEL2CAPStream *_Nullable stream, NSError *_Nullable error) { __typeof__(self) strongSelf = weakSelf; if (!strongSelf) { return; } dispatch_async(strongSelf->_queue, ^{ if (error) { handler(nil, error); } else { handler(stream, nil); } }); }]; } #pragma mark - GNCCentralManagerDelegate - (void)gnc_centralManagerDidUpdateState:(id)central { if (!_queue) { return; } dispatch_assert_queue(_queue); [self internalStartScanningIfPoweredOn]; } - (void)gnc_centralManager:(id)central didDiscoverPeripheral:(id)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI { dispatch_assert_queue(_queue); if (_advertisementFoundHandler) { _advertisementFoundHandler(peripheral, [self decodeAdvertisementData:advertisementData]); } } - (void)gnc_centralManager:(id)central didConnectPeripheral:(id)peripheral { dispatch_assert_queue(_queue); [self cancelConnectionTimeout]; if (_l2capPSM > 0) { [self internalOpenL2CAPChannel:peripheral]; return; } GNCGATTConnectionCompletionHandler handler = _gattConnectionCompletionHandlers[peripheral.identifier]; _gattConnectionCompletionHandlers[peripheral.identifier] = nil; if (handler) { GNCBLEGATTClient *client = [[GNCBLEGATTClient alloc] initWithPeripheral:peripheral requestDisconnectionHandler:^(id peripheral) { dispatch_async(_queue, ^{ [_centralManager cancelPeripheralConnection:peripheral]; }); }]; handler(client, nil); } } - (void)gnc_centralManager:(id)central didFailToConnectPeripheral:(id)peripheral error:(nullable NSError *)error { dispatch_assert_queue(_queue); [self cancelConnectionTimeout]; if (_l2capPSM > 0) { GNCOpenL2CAPStreamCompletionHandler l2capStreamCompletionhandler = _l2capStreamCompletionHandlers[peripheral.identifier]; _l2capStreamCompletionHandlers[peripheral.identifier] = nil; if (l2capStreamCompletionhandler) { l2capStreamCompletionhandler(nil, error); } return; } GNCGATTConnectionCompletionHandler handler = _gattConnectionCompletionHandlers[peripheral.identifier]; _gattConnectionCompletionHandlers[peripheral.identifier] = nil; if (handler) { handler(nil, error); } } - (void)gnc_centralManager:(id)central didDisconnectPeripheral:(id)peripheral error:(nullable NSError *)error { dispatch_assert_queue(_queue); GNCGATTDisconnectionHandler handler = _gattDisconnectionHandlers[peripheral.identifier]; _gattDisconnectionHandlers[peripheral.identifier] = nil; if (handler) { handler(); } } #pragma mark - CBCentralManagerDelegate - (void)centralManagerDidUpdateState:(CBCentralManager *)central { [self gnc_centralManagerDidUpdateState:central]; } - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI { [self gnc_centralManager:central didDiscoverPeripheral:peripheral advertisementData:advertisementData RSSI:RSSI]; } - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral { [self gnc_centralManager:central didConnectPeripheral:peripheral]; } - (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error { [self gnc_centralManager:central didFailToConnectPeripheral:peripheral error:error]; } - (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error { [self gnc_centralManager:central didDisconnectPeripheral:peripheral error:error]; } @end NS_ASSUME_NONNULL_END