From ec7dbcbf30ae642b43c358c3695ad482b4c1f75a Mon Sep 17 00:00:00 2001 From: Edwin Wu Date: Thu, 22 May 2025 01:08:36 -0700 Subject: [PATCH] Fix L2CAP stream connection problem. PiperOrigin-RevId: 761848851 --- .../apple/Mediums/BLEv2/GNCBLEMedium.m | 101 ++++++++++++++---- .../apple/Tests/GNCBLEMedium+Testing.h | 3 + .../apple/Tests/GNCBLEMediumTest.m | 23 ++++ 3 files changed, 104 insertions(+), 23 deletions(-) diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEMedium.m b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEMedium.m index 783a95b2..243689ab 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEMedium.m +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEMedium.m @@ -55,9 +55,16 @@ static GNCBLEL2CAPServer *_Nonnull CreateL2CapServer( // 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; @@ -68,11 +75,18 @@ static GNCBLEL2CAPServer *_Nonnull CreateL2CapServer( // 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 *_connectionCompletionHandlers; + NSMutableDictionary + *_gattConnectionCompletionHandlers; // A peripheral to disconnection handler map. Used to track when a peripheral becomes - // disconnected. Once disconnected, the completion handler is called and removed from the map. - NSMutableDictionary *_disconnectionHandlers; + // 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; } - (instancetype)init { @@ -93,9 +107,11 @@ static GNCBLEL2CAPServer *_Nonnull CreateL2CapServer( _queue = queue ?: dispatch_get_main_queue(); _centralManager = centralManager; _centralManager.centralDelegate = self; - _connectionCompletionHandlers = [NSMutableDictionary dictionary]; - _disconnectionHandlers = [NSMutableDictionary dictionary]; + _gattConnectionCompletionHandlers = [NSMutableDictionary dictionary]; + _gattDisconnectionHandlers = [NSMutableDictionary dictionary]; _scanningServiceUUIDs = [NSMutableArray array]; + _l2capStreamCompletionHandlers = [NSMutableDictionary dictionary]; + _l2capPSM = 0; } return self; } @@ -199,8 +215,9 @@ static GNCBLEL2CAPServer *_Nonnull CreateL2CapServer( completionHandler: (nullable GNCGATTConnectionCompletionHandler)completionHandler { dispatch_async(_queue, ^{ - _disconnectionHandlers[remotePeripheral.identifier] = disconnectionHandler; - _connectionCompletionHandlers[remotePeripheral.identifier] = completionHandler; + _gattDisconnectionHandlers[remotePeripheral.identifier] = disconnectionHandler; + _gattConnectionCompletionHandlers[remotePeripheral.identifier] = completionHandler; + _l2capPSM = 0; [_centralManager connectPeripheral:remotePeripheral options:@{}]; }); } @@ -232,19 +249,22 @@ static GNCBLEL2CAPServer *_Nonnull CreateL2CapServer( requestDisconnectionHandler:^(id _Nonnull peripheral){ }]; } - [_l2capClient openL2CAPChannelWithPSM:psm - completionHandler:^void(GNCBLEL2CAPStream *_Nullable stream, - NSError *_Nullable error) { - if (!completionHandler) return; - if (error) { - completionHandler(nil, error); - } else { - completionHandler(stream, nil); - } - }]; + _l2capStreamCompletionHandlers[remotePeripheral.identifier] = completionHandler; + _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. + [_centralManager connectPeripheral:remotePeripheral options:@{}]; }); } +// 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 { @@ -300,6 +320,35 @@ static GNCBLEL2CAPServer *_Nonnull CreateL2CapServer( return @{}; } +- (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 + 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 { @@ -320,8 +369,13 @@ static GNCBLEL2CAPServer *_Nonnull CreateL2CapServer( - (void)gnc_centralManager:(id)central didConnectPeripheral:(id)peripheral { dispatch_assert_queue(_queue); - GNCGATTConnectionCompletionHandler handler = _connectionCompletionHandlers[peripheral.identifier]; - _connectionCompletionHandlers[peripheral.identifier] = nil; + if (_l2capPSM > 0) { + [self internalOpenL2CAPChannel:peripheral]; + return; + } + GNCGATTConnectionCompletionHandler handler = + _gattConnectionCompletionHandlers[peripheral.identifier]; + _gattConnectionCompletionHandlers[peripheral.identifier] = nil; if (handler) { GNCBLEGATTClient *client = [[GNCBLEGATTClient alloc] initWithPeripheral:peripheral @@ -338,8 +392,9 @@ static GNCBLEL2CAPServer *_Nonnull CreateL2CapServer( didFailToConnectPeripheral:(id)peripheral error:(nullable NSError *)error { dispatch_assert_queue(_queue); - GNCGATTConnectionCompletionHandler handler = _connectionCompletionHandlers[peripheral.identifier]; - _connectionCompletionHandlers[peripheral.identifier] = nil; + GNCGATTConnectionCompletionHandler handler = + _gattConnectionCompletionHandlers[peripheral.identifier]; + _gattConnectionCompletionHandlers[peripheral.identifier] = nil; if (handler) { handler(nil, error); } @@ -349,8 +404,8 @@ static GNCBLEL2CAPServer *_Nonnull CreateL2CapServer( didDisconnectPeripheral:(id)peripheral error:(nullable NSError *)error { dispatch_assert_queue(_queue); - GNCGATTDisconnectionHandler handler = _disconnectionHandlers[peripheral.identifier]; - _disconnectionHandlers[peripheral.identifier] = nil; + GNCGATTDisconnectionHandler handler = _gattDisconnectionHandlers[peripheral.identifier]; + _gattDisconnectionHandlers[peripheral.identifier] = nil; if (handler) { handler(); } diff --git a/internal/platform/implementation/apple/Tests/GNCBLEMedium+Testing.h b/internal/platform/implementation/apple/Tests/GNCBLEMedium+Testing.h index f0867c2b..4eea8d87 100644 --- a/internal/platform/implementation/apple/Tests/GNCBLEMedium+Testing.h +++ b/internal/platform/implementation/apple/Tests/GNCBLEMedium+Testing.h @@ -38,6 +38,9 @@ NS_ASSUME_NONNULL_BEGIN - (NSDictionary *)decodeAdvertisementData: (NSDictionary *)advertisementData; + +- (void)setL2CAPClient:(GNCBLEL2CAPClient *)l2capClient; + @end NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Tests/GNCBLEMediumTest.m b/internal/platform/implementation/apple/Tests/GNCBLEMediumTest.m index 6a431852..80582d29 100644 --- a/internal/platform/implementation/apple/Tests/GNCBLEMediumTest.m +++ b/internal/platform/implementation/apple/Tests/GNCBLEMediumTest.m @@ -21,6 +21,7 @@ #import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTClient.h" #import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h" #import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h" +#import "internal/platform/implementation/apple/Tests/GNCBLEL2CAPClient+Testing.h" #import "internal/platform/implementation/apple/Tests/GNCBLEMedium+Testing.h" #import "internal/platform/implementation/apple/Tests/GNCFakeCentralManager.h" #import "internal/platform/implementation/apple/Tests/GNCFakePeripheral.h" @@ -304,6 +305,28 @@ static NSString *const kServiceUUID = @"0000FEF3-0000-1000-8000-00805F9B34FB"; [self waitForExpectations:@[ channelOpenedexpectation ] timeout:0.5]; } +- (void)testSuccessfulOpenL2CAPChannel { + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; + XCTestExpectation *expectation = + [[XCTestExpectation alloc] initWithDescription:@"Open L2CAP channel."]; + + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + GNCBLEL2CAPClient *l2capClient = + [[GNCBLEL2CAPClient alloc] initWithPeripheral:fakePeripheral + queue:nil + requestDisconnectionHandler:^(id _Nonnull peripheral) { + }]; + [medium setL2CAPClient:l2capClient]; + [medium openL2CAPChannelWithPSM:123 + peripheral:fakePeripheral + completionHandler:^(GNCBLEL2CAPStream *_Nullable stream, NSError *_Nullable error) { + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:3]; +} + #pragma mark - Connect - (void)testSuccessfulConnect {