From 675e4f738fb68fcf5deaf5baffc560965a2c259f Mon Sep 17 00:00:00 2001 From: Edwin Wu Date: Wed, 9 Apr 2025 21:46:44 -0700 Subject: [PATCH] Refactor the GNCBLEL2CAPServer. PiperOrigin-RevId: 745855701 --- .../apple/Mediums/BLEv2/GNCBLEError.h | 1 + .../apple/Mediums/BLEv2/GNCBLEL2CAPServer.h | 31 +++- .../apple/Mediums/BLEv2/GNCBLEL2CAPServer.m | 132 ++++++++++++------ .../platform/implementation/apple/Tests/BUILD | 1 - .../apple/Tests/GNCBLEL2CAPServerTest.m | 88 +++++++++--- .../apple/Tests/GNCFakePeripheralManager.h | 3 + .../apple/Tests/GNCFakePeripheralManager.m | 4 + 7 files changed, 194 insertions(+), 66 deletions(-) diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEError.h b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEError.h index 0db6d168..ee5e8b0e 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEError.h +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEError.h @@ -27,4 +27,5 @@ typedef NS_ERROR_ENUM(GNCBLEErrorDomain, GNCBLEError){ GNCBLEErrorAlreadyDiscoveringSpecifiedCharacteristics, GNCBLEErrorAlreadyReadingCharacteristic, GNCBLEErrorAlreadyScanning, + GNCBLEErrorL2CAPListeningOnQueueNil, }; diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPServer.h b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPServer.h index 5097f5f6..e1fcc6b1 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPServer.h +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPServer.h @@ -15,8 +15,17 @@ #import #import +@protocol GNCPeripheralManager; + NS_ASSUME_NONNULL_BEGIN +/** + * Completion handler for starting listening for an L2CAP channel. + * + * @param error The cause of the failure, or @c nil if no error occurred. + */ +typedef void (^GNCStartListeningL2CAPChannelCompletionHandler)(NSError *_Nullable error); + /** * An object that publishes the @c PSM value. * @@ -28,11 +37,27 @@ NS_ASSUME_NONNULL_BEGIN @property(atomic, readonly) CBL2CAPPSM PSM; /** - * Retrieves the L2CAP channel. + * Initializes the L2CAP server. * - * @return The L2CAP channel, or nil if the channel is not available. + * @param peripheralManager The peripheral manager to use. If @c nil, a default peripheral manager + * will be used. + * @param queue The queue to use. If @c nil, then by default it will be DISPATCH_QUEUE_SERIAL. */ -- (nullable CBL2CAPChannel *)getChannel; +- (instancetype)initWithPeripheralManager:(nullable id)peripheralManager + queue:(nullable dispatch_queue_t)queue; + +/** + * Starts listening for an L2CAP channel. + * + * @param completionHandler The completion handler to call when the L2CAP channel is started. + */ +- (void)startListeningChannelWithCompletionHandler: + (GNCStartListeningL2CAPChannelCompletionHandler)completionHandler; + +/** + * Closes the L2CAP channel. + */ +- (void)close; @end diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPServer.m b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPServer.m index 7ef2ba42..4eee6e56 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPServer.m +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPServer.m @@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN -static char *const kGNCBLEL2CAPServerQueueLabel = "com.nearby.GNCBLEL2CAPServer"; +static char *const kGNCBLEL2CAPServerQueueLabel = "com.google.nearby.GNCBLEL2CAPServer"; @interface GNCBLEL2CAPServer () @property(atomic, readwrite) CBL2CAPPSM PSM; @@ -32,37 +32,67 @@ static char *const kGNCBLEL2CAPServerQueueLabel = "com.nearby.GNCBLEL2CAPServer" @implementation GNCBLEL2CAPServer { dispatch_queue_t _queue; id _peripheralManager; + NSString *_serviceID; + GNCStartListeningL2CAPChannelCompletionHandler _startListeningL2CAPChannelcompletionHandler; - // The L2CAP channel that is used to send and receive data. - CBL2CAPChannel *_channel; + CBL2CAPChannel *_l2CAPChannel; + + /// Whether start call has been performed when the peripheral was off. + BOOL _alreadyStartedWhenPeripheralPoweredOff; } - (instancetype)init { - self = [super init]; - if (self) { - _queue = dispatch_queue_create(kGNCBLEL2CAPServerQueueLabel, DISPATCH_QUEUE_SERIAL); - _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:nil queue:_queue]; - // Set for @c GNCPeripheralManager to be able to forward callbacks. - _peripheralManager.peripheralDelegate = self; - } - return self; + 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 { +- (instancetype)initWithPeripheralManager:(nullable id)peripheralManager + queue:(nullable dispatch_queue_t)queue { self = [super init]; if (self) { - _queue = dispatch_get_main_queue(); - _peripheralManager = peripheralManager; - // Set for @c GNCPeripheralManager to be able to forward callbacks. - _peripheralManager.peripheralDelegate = 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; } -- (nullable CBL2CAPChannel *)getChannel { - return _channel; +- (void)startListeningChannelWithCompletionHandler: + (GNCStartListeningL2CAPChannelCompletionHandler)completionHandler { + _startListeningL2CAPChannelcompletionHandler = [completionHandler copy]; + if (!_queue) { + _startListeningL2CAPChannelcompletionHandler([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 { + GTMLoggerInfo(@"[NEARBY] Peripheral must be on to start, waiting."); + _alreadyStartedWhenPeripheralPoweredOff = YES; + } +} + +- (void)close { + [self shutDown]; } #pragma mark - GNCPeripheralManagerDelegate @@ -70,10 +100,18 @@ static char *const kGNCBLEL2CAPServerQueueLabel = "com.nearby.GNCBLEL2CAPServer" - (void)gnc_peripheralManagerDidUpdateState:(id)peripheral { dispatch_assert_queue(_queue); if (_peripheralManager.state == CBManagerStatePoweredOn) { - [_peripheralManager publishL2CAPChannelWithEncryption:NO]; + 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) { - [_peripheralManager unpublishL2CAPChannel:_PSM]; + [self shutDown]; } } @@ -81,19 +119,27 @@ static char *const kGNCBLEL2CAPServerQueueLabel = "com.nearby.GNCBLEL2CAPServer" didPublishL2CAPChannel:(CBL2CAPPSM)PSM error:(nullable NSError *)error { dispatch_assert_queue(_queue); + GTMLoggerDebug(@"[NEARBY] didPublishL2CAPChannel with PSM: %@", @(PSM)); if (error) { - GTMLoggerError(@"Failed to publish L2CAP channel: %@", error); + GTMLoggerError(@"[NEARBY] Failed to publish L2CAP channel: %@", error); + if (_startListeningL2CAPChannelcompletionHandler) { + _startListeningL2CAPChannelcompletionHandler(error); + } + return; } - GTMLoggerInfo(@"Published L2CAP channel with PSM: %d", PSM); _PSM = PSM; + if (_startListeningL2CAPChannelcompletionHandler) { + _startListeningL2CAPChannelcompletionHandler(nil); + } } - (void)gnc_peripheralManager:(id)peripheral didUnpublishL2CAPChannel:(CBL2CAPPSM)PSM error:(NSError *)error { dispatch_assert_queue(_queue); + GTMLoggerDebug(@"[NEARBY] didUnpublishL2CAPChannel on PSM %@", @(PSM)); if (error) { - GTMLoggerError(@"Failed to unpublish L2CAP channel: %@", error); + GTMLoggerError(@"[NEARBY] Failed to unpublish L2CAP channel: %@", error); } _PSM = 0; } @@ -102,50 +148,52 @@ static char *const kGNCBLEL2CAPServerQueueLabel = "com.nearby.GNCBLEL2CAPServer" didOpenL2CAPChannel:(nullable CBL2CAPChannel *)channel error:(nullable NSError *)error { dispatch_assert_queue(_queue); + GTMLoggerDebug(@"[NEARBY] didOpenL2CAPChannel"); if (error) { - GTMLoggerError(@"Failed to open L2CAP channel: %@", error); + GTMLoggerError(@"[NEARBY] Failed to open L2CAP channel: %@", error); return; } - GTMLoggerInfo(@"Opened L2CAP channel with PSM: %d", channel.PSM); - _channel = channel; - // TODO: b/399815436 - Implement to wrap up l2cap channel. + + // Cleanup older references. + if (_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; + // TODO: b/399815436 - Implement to wrap up l2cap channel with |GNCBLEL2CAPStream|. } #pragma mark - CBPeripheralManagerDelegate - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral { - dispatch_async(_queue, ^{ - [self gnc_peripheralManagerDidUpdateState:peripheral]; - }); + [self gnc_peripheralManagerDidUpdateState:peripheral]; } - (void)peripheralManager:(CBPeripheralManager *)peripheral didPublishL2CAPChannel:(CBL2CAPPSM)PSM error:(nullable NSError *)error { - dispatch_async(_queue, ^{ - [self gnc_peripheralManager:peripheral didPublishL2CAPChannel:PSM error:error]; - }); + [self gnc_peripheralManager:peripheral didPublishL2CAPChannel:PSM error:error]; } - (void)peripheralManager:(CBPeripheralManager *)peripheral didUnpublishL2CAPChannel:(CBL2CAPPSM)PSM error:(nullable NSError *)error { - dispatch_async(_queue, ^{ - [self gnc_peripheralManager:peripheral didUnpublishL2CAPChannel:PSM error:error]; - }); + [self gnc_peripheralManager:peripheral didUnpublishL2CAPChannel:PSM error:error]; } - (void)peripheralManager:(CBPeripheralManager *)peripheral didOpenL2CAPChannel:(nullable CBL2CAPChannel *)channel error:(nullable NSError *)error { - dispatch_async(_queue, ^{ - [self gnc_peripheralManager:peripheral didOpenL2CAPChannel:channel error:error]; - }); + [self gnc_peripheralManager:peripheral didOpenL2CAPChannel:channel error:error]; } #pragma mark Private -- (void)tearDown { +- (void)shutDown { if (_PSM > 0) { [_peripheralManager unpublishL2CAPChannel:_PSM]; } @@ -153,6 +201,10 @@ static char *const kGNCBLEL2CAPServerQueueLabel = "com.nearby.GNCBLEL2CAPServer" _peripheralManager = nil; } +- (void)closeL2CAPChannel { + _l2CAPChannel = nil; +} + @end NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Tests/BUILD b/internal/platform/implementation/apple/Tests/BUILD index 92f60c95..ab5795d9 100644 --- a/internal/platform/implementation/apple/Tests/BUILD +++ b/internal/platform/implementation/apple/Tests/BUILD @@ -30,7 +30,6 @@ objc_library( "GNCBLEGATTServerTest.m", "GNCBLEL2CAPFakeInputOutputStream.h", "GNCBLEL2CAPFakeInputOutputStream.m", - "GNCBLEL2CAPServer+Testing.h", "GNCBLEL2CAPServerTest.m", "GNCBLEL2CAPStreamTest.m", "GNCBLEMedium+Testing.h", diff --git a/internal/platform/implementation/apple/Tests/GNCBLEL2CAPServerTest.m b/internal/platform/implementation/apple/Tests/GNCBLEL2CAPServerTest.m index af1981c9..7aaebd70 100644 --- a/internal/platform/implementation/apple/Tests/GNCBLEL2CAPServerTest.m +++ b/internal/platform/implementation/apple/Tests/GNCBLEL2CAPServerTest.m @@ -18,58 +18,102 @@ #import #import -#import "internal/platform/implementation/apple/Tests/GNCBLEL2CAPServer+Testing.h" #import "internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.h" -static const int kPSM = 192; - @interface GNCBLEL2CAPServerTest : XCTestCase @end @implementation GNCBLEL2CAPServerTest -#pragma mark - Publish L2CAP channel +#pragma mark Tests -- (void)testPublishL2CAPChannel { +- (void)testPublishL2CAPChannelWhenStartListeningChannel { GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; - - fakePeripheralManager.PSM = kPSM; GNCBLEL2CAPServer *l2capServer = - [[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager]; - + [[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; [fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn]; - XCTAssertEqual([l2capServer PSM], kPSM); - XCTAssertNotNil([l2capServer getChannel]); + [l2capServer startListeningChannelWithCompletionHandler:^(NSError *error) { + XCTAssertEqual(error, nil); + XCTAssertEqual([l2capServer PSM], fakePeripheralManager.PSM); + }]; } - (void)testFailedToPublishL2CAPChannel { GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; - fakePeripheralManager.didPublishL2CAPChannelError = [NSError errorWithDomain:@"fake" code:0 userInfo:nil]; - fakePeripheralManager.PSM = kPSM; GNCBLEL2CAPServer *l2capServer = - [[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager]; - + [[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; [fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn]; - XCTAssertNotEqual([l2capServer PSM], kPSM); - XCTAssertNil([l2capServer getChannel]); + [l2capServer startListeningChannelWithCompletionHandler:^(NSError *error) { + XCTAssertEqual(error, fakePeripheralManager.didPublishL2CAPChannelError); + XCTAssertEqual([l2capServer PSM], 0); + }]; } -- (void)testUnPublishL2CAPChannel { +- (void)testPoweredOffUnpublishesChannel { GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; - - fakePeripheralManager.PSM = kPSM; GNCBLEL2CAPServer *l2capServer = - [[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager]; + [[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + [fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn]; + + [l2capServer startListeningChannelWithCompletionHandler:^(NSError *error) { + XCTAssertEqual(error, nil); + XCTAssertEqual([l2capServer PSM], fakePeripheralManager.PSM); + }]; + + [fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOff]; + + [self waitForExpectations:@[ fakePeripheralManager.unpublishExpectation ] timeout:0.0]; +} + +- (void)testStartPeripheralManagerInitiallyOff { + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEL2CAPServer *l2capServer = + [[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + + [l2capServer startListeningChannelWithCompletionHandler:^(NSError *error) { + XCTAssertEqual(error, nil); + XCTAssertEqual([l2capServer PSM], fakePeripheralManager.PSM); + }]; [fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn]; - [fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOff]; +} + +- (void)testClose { + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEL2CAPServer *l2capServer = + [[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + [fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn]; + + [l2capServer startListeningChannelWithCompletionHandler:^(NSError *error) { + XCTAssertEqual(error, nil); + XCTAssertEqual([l2capServer PSM], fakePeripheralManager.PSM); + }]; + + [l2capServer close]; XCTAssertEqual([l2capServer PSM], 0); } +- (void)testCloseDoesNotUnpublishesChannelIfNotConnected { + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + fakePeripheralManager.unpublishExpectation.inverted = YES; + GNCBLEL2CAPServer *l2capServer = + [[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + + [l2capServer close]; + + [self waitForExpectations:@[ fakePeripheralManager.unpublishExpectation ] timeout:1.0]; +} + @end diff --git a/internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.h b/internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.h index 3ae71aca..7326ab4f 100644 --- a/internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.h +++ b/internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.h @@ -44,6 +44,9 @@ NS_ASSUME_NONNULL_BEGIN /** Expectation fulfilled when peripheral responds to a request with an error. */ @property(nonatomic, readonly) XCTestExpectation *respondToRequestErrorExpectation; +/** Expectation fulfilled when peripheral unpublishes an L2CAP channel. */ +@property(nonatomic, readonly) XCTestExpectation *unpublishExpectation; + /** * Similates an @c addService: error. * diff --git a/internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.m b/internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.m index b7bc99c2..0d6c67a4 100644 --- a/internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.m +++ b/internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.m @@ -72,10 +72,13 @@ initWithDescription:@"Fulfilled when peripheral responds to a request with success."]; _respondToRequestErrorExpectation = [[XCTestExpectation alloc] initWithDescription:@"Fulfilled when peripheral responds to a request with an error."]; + _unpublishExpectation = [[XCTestExpectation alloc] + initWithDescription:@"Fulfilled when L2CAP channel is unpublished."]; _isAdvertising = NO; _state = CBManagerStateUnknown; _advertisementData = nil; _services = [[NSMutableArray alloc] init]; + _PSM = 192; } return self; } @@ -141,6 +144,7 @@ [peripheralDelegate gnc_peripheralManager:self didUnpublishL2CAPChannel:_PSM error:_didUnPublishL2CAPChannelError]; + [_unpublishExpectation fulfill]; } #pragma mark - Testing Helpers