// Copyright 2025 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/GNCBLEL2CAPServer.h" #import #import #import "internal/platform/implementation/apple/Log/GNCLogger.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEError.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEL2CAPStream.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCPeripheralManager.h" NS_ASSUME_NONNULL_BEGIN static char *const kGNCBLEL2CAPServerQueueLabel = "com.google.nearby.GNCBLEL2CAPServer"; @interface GNCBLEL2CAPServer () @property(atomic, readwrite) CBL2CAPPSM PSM; @end @implementation GNCBLEL2CAPServer { dispatch_queue_t _queue; id _peripheralManager; NSString *_serviceID; GNCOpenL2CAPServerPSMPublishedCompletionHandler _psmPublishedCompletionHandler; GNCOpenL2CAPServerChannelOpendCompletionHandler _channelOpenedCompletionHandler; CBL2CAPChannel *_l2CAPChannel; GNCBLEL2CAPStream *_l2CAPStream; /// Whether start call has been performed when the peripheral was off. BOOL _alreadyStartedWhenPeripheralPoweredOff; } - (instancetype)init { return [self initWithPeripheralManager:nil queue:nil]; } // This is private and should only be used for tests. The provided peripheral manager must call // delegate methods on the main queue. - (instancetype)initWithPeripheralManager:(nullable id)peripheralManager queue:(nullable dispatch_queue_t)queue { self = [super init]; if (self) { _queue = queue ?: dispatch_queue_create(kGNCBLEL2CAPServerQueueLabel, DISPATCH_QUEUE_SERIAL); if (peripheralManager) { _peripheralManager = peripheralManager; // Set for @c GNCPeripheralManager to be able to forward callbacks. _peripheralManager.peripheralDelegate = self; } } return self; } - (void)startListeningChannelWithPSMPublishedCompletionHandler: (GNCOpenL2CAPServerPSMPublishedCompletionHandler)psmPublishedCompletionHandler channelOpenedCompletionHandler: (GNCOpenL2CAPServerChannelOpendCompletionHandler) channelOpenedCompletionHandler { _psmPublishedCompletionHandler = [psmPublishedCompletionHandler copy]; _channelOpenedCompletionHandler = [channelOpenedCompletionHandler copy]; if (!_queue) { _psmPublishedCompletionHandler(0, [NSError errorWithDomain:GNCBLEErrorDomain code:GNCBLEErrorL2CAPListeningOnQueueNil userInfo:nil]); return; } if (!_peripheralManager) { // Lazy initialization to avoid system dialog on app startup before pairing. _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:nil queue:_queue options:nil]; // Set for @c GNCPeripheralManager to be able to forward callbacks. _peripheralManager.peripheralDelegate = self; } if (_peripheralManager.state == CBManagerStatePoweredOn) { // Bluetooth link is already encrypted, however encryption is not required here to avoid getting // insufficient authentication errors due to initialization order. [_peripheralManager publishL2CAPChannelWithEncryption:NO]; } else { GNCLoggerInfo(@"[NEARBY] Peripheral must be on to start, waiting."); _alreadyStartedWhenPeripheralPoweredOff = YES; } } - (void)close { [self shutDown]; } #pragma mark - GNCPeripheralManagerDelegate - (void)gnc_peripheralManagerDidUpdateState:(id)peripheral { dispatch_assert_queue(_queue); if (_peripheralManager.state == CBManagerStatePoweredOn) { if (_alreadyStartedWhenPeripheralPoweredOff) { // Only setup once so that toggling Bluetooth does not cause an L2CAP channel to be // published every time since a new instance is created for each new channel. _alreadyStartedWhenPeripheralPoweredOff = NO; // Bluetooth link is already encrypted, however encryption is not required here to avoid // getting insufficient authentication errors due to initialization order. [_peripheralManager publishL2CAPChannelWithEncryption:NO]; } } if (_peripheralManager.state == CBManagerStatePoweredOff) { [self shutDown]; } } - (void)gnc_peripheralManager:(id)peripheral didPublishL2CAPChannel:(CBL2CAPPSM)PSM error:(nullable NSError *)error { dispatch_assert_queue(_queue); GNCLoggerDebug(@"[NEARBY] didPublishL2CAPChannel with PSM: %@", @(PSM)); if (error) { GNCLoggerError(@"[NEARBY] Failed to publish L2CAP channel: %@", error); if (_psmPublishedCompletionHandler) { _psmPublishedCompletionHandler(0, error); } return; } _PSM = PSM; if (_psmPublishedCompletionHandler) { _psmPublishedCompletionHandler(PSM, nil); } } - (void)gnc_peripheralManager:(id)peripheral didUnpublishL2CAPChannel:(CBL2CAPPSM)PSM error:(NSError *)error { dispatch_assert_queue(_queue); GNCLoggerDebug(@"[NEARBY] didUnpublishL2CAPChannel on PSM %@", @(PSM)); if (error) { GNCLoggerError(@"[NEARBY] Failed to unpublish L2CAP channel: %@", error); } [_l2CAPStream tearDown]; _l2CAPStream = nil; _PSM = 0; } - (void)gnc_peripheralManager:(id)peripheral didOpenL2CAPChannel:(nullable CBL2CAPChannel *)channel error:(nullable NSError *)error { dispatch_assert_queue(_queue); GNCLoggerDebug( @"[NEARBY] didOpenL2CAPChannel, channel: %@, inputStream: %@, outputStream: %@, error: %@", channel, channel.inputStream, channel.outputStream, error); // TODO: edwinwu - channel.inputStream is null when doing testing. Refactor tests in the future. if (error || (channel && (!channel.inputStream || !channel.outputStream))) { if (_channelOpenedCompletionHandler) { _channelOpenedCompletionHandler(nil, error); } return; } // Cleanup older references. if (_l2CAPStream || _l2CAPChannel) { // The device may establish a new L2CAP socket connection while the old socket is still // connected if sysproxy stopped for a reason other than Bluetooth disconnection. Closing the // channel here ensures the server and the client state is reset between the two // connection sessions. [self closeL2CAPChannel]; } _l2CAPChannel = channel; __weak __typeof__(self) weakSelf = self; _l2CAPStream = [[GNCBLEL2CAPStream alloc] initWithClosedBlock:^{ __typeof__(self) strongSelf = weakSelf; // Indicates the L2CAP socket is closed. Clean up the resources used for the old // connection so that a new one can be established. [strongSelf closeL2CAPChannel]; } inputStream:_l2CAPChannel.inputStream outputStream:_l2CAPChannel.outputStream]; if (_channelOpenedCompletionHandler) { _channelOpenedCompletionHandler(_l2CAPStream, nil); } } #pragma mark - CBPeripheralManagerDelegate - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral { [self gnc_peripheralManagerDidUpdateState:peripheral]; } - (void)peripheralManager:(CBPeripheralManager *)peripheral didPublishL2CAPChannel:(CBL2CAPPSM)PSM error:(nullable NSError *)error { [self gnc_peripheralManager:peripheral didPublishL2CAPChannel:PSM error:error]; } - (void)peripheralManager:(CBPeripheralManager *)peripheral didUnpublishL2CAPChannel:(CBL2CAPPSM)PSM error:(nullable NSError *)error { [self gnc_peripheralManager:peripheral didUnpublishL2CAPChannel:PSM error:error]; } - (void)peripheralManager:(CBPeripheralManager *)peripheral didOpenL2CAPChannel:(nullable CBL2CAPChannel *)channel error:(nullable NSError *)error { [self gnc_peripheralManager:peripheral didOpenL2CAPChannel:channel error:error]; } #pragma mark Private - (void)shutDown { if (_PSM > 0) { [_peripheralManager unpublishL2CAPChannel:_PSM]; } _peripheralManager.peripheralDelegate = nil; _peripheralManager = nil; } - (void)closeL2CAPChannel { [_l2CAPStream tearDown]; _l2CAPStream = nil; _l2CAPChannel = nil; } @end NS_ASSUME_NONNULL_END