diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPClient.h b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPClient.h index 28cb790f..4a98b817 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPClient.h +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPClient.h @@ -14,10 +14,16 @@ #import +#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.h" + @protocol GNCPeripheral; NS_ASSUME_NONNULL_BEGIN +/// Completion handler for opening an L2CAP stream. +typedef void (^GNCOpenL2CAPStreamCompletionHandler)(GNCBLEL2CAPStream *_Nullable stream, + NSError *_Nullable error); + /** * A block to be invoked after a call to @c disconnect, requesting that the local connection to the * remote peripheral be cancelled. @@ -49,9 +55,11 @@ typedef void (^GNCRequestDisconnectionHandler)(id peripheral); * Opens a L2CAP channel with the @c PSM. * * @param PSM The PSM to use for opening the L2CAP channel. + * @param completionHandler Called on a private queue with the opened L2CAP stream if successfully + * opened or an error if one has occurred. */ -// TODO: b/399815436 - Add CompletionHandler for this method. -- (void)openL2CAPChannelWithPSM:(uint16_t)PSM; +- (void)openL2CAPChannelWithPSM:(uint16_t)PSM + completionHandler:(GNCOpenL2CAPStreamCompletionHandler)completionHandler; /** Cancels an active or pending local connection to a peripheral. */ - (void)disconnect; diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPClient.m b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPClient.m index 6bf09ceb..78157ed0 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPClient.m +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPClient.m @@ -18,10 +18,11 @@ #import #import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEError.h" +#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.h" #import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h" #import "GoogleToolboxForMac/GTMLogger.h" -static char *const kGNCBLEL2CAPClientQueueLabel = "com.googlenearby.GNCBLEL2CAPClient"; +static char *const kGNCBLEL2CAPClientQueueLabel = "com.google.nearby.GNCBLEL2CAPClient"; @interface GNCBLEL2CAPClient () @end @@ -29,10 +30,11 @@ static char *const kGNCBLEL2CAPClientQueueLabel = "com.googlenearby.GNCBLEL2CAPC @implementation GNCBLEL2CAPClient { dispatch_queue_t _queue; id _peripheral; + GNCOpenL2CAPStreamCompletionHandler _completionHandler; GNCRequestDisconnectionHandler _requestDisconnectionHandler; - // The L2CAP channel that is used to send and receive data. CBL2CAPChannel *_l2CAPChannel; + GNCBLEL2CAPStream *_l2CAPStream; } - (instancetype)initWithPeripheral:(id)peripheral @@ -58,8 +60,11 @@ static char *const kGNCBLEL2CAPClientQueueLabel = "com.googlenearby.GNCBLEL2CAPC return self; }; -- (void)openL2CAPChannelWithPSM:(uint16_t)PSM { - [_peripheral openL2CAPChannelWithPSM:PSM]; +- (void)openL2CAPChannelWithPSM:(uint16_t)PSM + completionHandler:(GNCOpenL2CAPStreamCompletionHandler)completionHandler { + GTMLoggerInfo(@"[NEARBY] openL2CAPChannelWithPSM = %d", PSM); + _completionHandler = [completionHandler copy]; + [_peripheral openL2CAPChannel:(CBL2CAPPSM)PSM]; } - (void)disconnect { @@ -70,16 +75,45 @@ static char *const kGNCBLEL2CAPClientQueueLabel = "com.googlenearby.GNCBLEL2CAPC #pragma mark - GNCPeripheralDelegate -- (void)gnc_peripheral:(id)peripheral didOpenL2CAPChannel:(CBL2CAPChannel *)channel - error:(NSError *)error { +- (void)gnc_peripheral:(id)peripheral + didOpenL2CAPChannel:(CBL2CAPChannel *)channel + error:(NSError *)error { dispatch_assert_queue(_queue); - if (error) { - GTMLoggerError(@"[NEARBY] Failed to open L2CAP channel: %@", error); + GTMLoggerDebug( + @"[NEARBY] didOpenL2CAPChannel, channel: %@, inputStream: %@, outputStream: %@, error: %@", + channel, channel.inputStream, channel.outputStream, error); + // TODO: b/399815436 - channel.inputStream is null when doing testing. Refactor tests in the + // future. + if (error || (channel && (!channel.inputStream || !channel.outputStream))) { + if (_completionHandler) { + _completionHandler(nil, error); + } return; } - GTMLoggerInfo(@"[NEARBY] Opened L2CAP channel: %@", channel); + + // Cleanup older references. + if (_l2CAPStream || _l2CAPChannel) { + // The watch 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 controller and the client state is reset between the two + // connection sessions. + [self closeL2CAPChannel]; + } + _l2CAPChannel = channel; - // TODO: b/399815436 - Implement to wrap up l2cap 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 (_completionHandler) { + _completionHandler(_l2CAPStream, nil); + } } #pragma mark - CBPeripheralDelegate @@ -87,9 +121,24 @@ static char *const kGNCBLEL2CAPClientQueueLabel = "com.googlenearby.GNCBLEL2CAPC - (void)peripheral:(CBPeripheral *)peripheral didOpenL2CAPChannel:(CBL2CAPChannel *)channel error:(NSError *)error { - if ([self respondsToSelector:@selector(gnc_peripheral:didOpenL2CAPChannel:error:)]) { - [self gnc_peripheral:peripheral didOpenL2CAPChannel:channel error:error]; - } + dispatch_async(_queue, ^{ + if ([self respondsToSelector:@selector(gnc_peripheral:didOpenL2CAPChannel:error:)]) { + [self gnc_peripheral:peripheral didOpenL2CAPChannel:channel error:error]; + } + }); +} + +#pragma mark Private + +- (void)shutDown { + _peripheral = nil; + _peripheral.peripheralDelegate = nil; +} + +- (void)closeL2CAPChannel { + [_l2CAPStream tearDown]; + _l2CAPStream = nil; + _l2CAPChannel = nil; } @end diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.m b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.m index 16e83a53..c2e1f459 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.m +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.m @@ -77,7 +77,6 @@ enum { READ_BUFFER_SIZE = 409600 }; inputStream:(NSInputStream *)inputStream outputStream:(NSOutputStream *)outputStream { self = [super init]; - if (self) { _streamQueue = dispatch_queue_create("com.google.nearby.GNCBLEL2CAPStream", dispatch_queue_attr_make_with_qos_class( diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEMedium.h b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEMedium.h index 5d5b2463..86460b02 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEMedium.h +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEMedium.h @@ -15,8 +15,9 @@ #import #import +#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPClient.h" #import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPServer.h" -#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.h" +// #import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.h" @class GNCBLEGATTServer; @class GNCBLEGATTClient; @@ -217,9 +218,12 @@ typedef void (^GNCGATTConnectionCompletionHandler)(GNCBLEGATTClient *_Nullable c * * @param PSM The PSM to use for opening the L2CAP channel. * @param remotePeripheral The peripheral to which the L2CAP channel is being opened. + * @param completionHandler Called on a private queue with the opened L2CAP stream if successfully + * opened or an error if one has occurred. */ -// TODO: b/399815436 - Add CompletionHandler for this method. -- (void)openL2CAPChannelWithPSM:(uint16_t)PSM peripheral:(id)remotePeripheral; +- (void)openL2CAPChannelWithPSM:(uint16_t)PSM + peripheral:(id)remotePeripheral + completionHandler:(nullable GNCOpenL2CAPStreamCompletionHandler)completionHandler; @end diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEMedium.m b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEMedium.m index e8f410cb..e14e7e18 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEMedium.m +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEMedium.m @@ -20,8 +20,8 @@ #import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEError.h" #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/GNCBLEL2CAPServer.h" #import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPClient.h" +#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPServer.h" #import "internal/platform/implementation/apple/Mediums/BLEv2/GNCCentralManager.h" #import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h" #import "internal/platform/implementation/apple/Mediums/BLEv2/NSData+GNCBase85.h" @@ -221,7 +221,9 @@ static GNCBLEL2CAPServer *_Nonnull CreateL2CapServer( }); } -- (void)openL2CAPChannelWithPSM:(uint16_t)PSM peripheral:(id)remotePeripheral { +- (void)openL2CAPChannelWithPSM:(uint16_t)psm + peripheral:(id)remotePeripheral + completionHandler:(nullable GNCOpenL2CAPStreamCompletionHandler)completionHandler { dispatch_async(_queue, ^{ if (!_l2capClient) { _l2capClient = @@ -229,7 +231,16 @@ static GNCBLEL2CAPServer *_Nonnull CreateL2CapServer( requestDisconnectionHandler:^(id _Nonnull peripheral){ }]; } - [_l2capClient openL2CAPChannelWithPSM:PSM]; + [_l2capClient openL2CAPChannelWithPSM:psm + completionHandler:^void(GNCBLEL2CAPStream *_Nullable stream, + NSError *_Nullable error) { + if (!completionHandler) return; + if (error) { + completionHandler(nil, error); + } else { + completionHandler(stream, nil); + } + }]; }); } diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h b/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h index 31d872d0..0092331e 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h @@ -122,7 +122,7 @@ NS_ASSUME_NONNULL_BEGIN * * @param PSM The PSM value to use for the L2CAP channel. */ -- (void)openL2CAPChannelWithPSM:(uint16_t)PSM; +- (void)openL2CAPChannel:(CBL2CAPPSM)PSM; @end diff --git a/internal/platform/implementation/apple/Tests/BUILD b/internal/platform/implementation/apple/Tests/BUILD index 4ff67ab7..efde10a3 100644 --- a/internal/platform/implementation/apple/Tests/BUILD +++ b/internal/platform/implementation/apple/Tests/BUILD @@ -28,6 +28,7 @@ objc_library( "GNCBLEGATTClientTest.m", "GNCBLEGATTServer+Testing.h", "GNCBLEGATTServerTest.m", + "GNCBLEL2CAPClient+Testing.h", "GNCBLEL2CAPClientTest.m", "GNCBLEL2CAPConnectionTest.m", "GNCBLEL2CAPFakeInputOutputStream.h", diff --git a/internal/platform/implementation/apple/Tests/GNCBLEL2CAPClient+Testing.h b/internal/platform/implementation/apple/Tests/GNCBLEL2CAPClient+Testing.h new file mode 100644 index 00000000..f0078e11 --- /dev/null +++ b/internal/platform/implementation/apple/Tests/GNCBLEL2CAPClient+Testing.h @@ -0,0 +1,42 @@ +// 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/BLEv2/GNCBLEL2CAPClient.h" + +#import + +@protocol GNCPeripheral; + +NS_ASSUME_NONNULL_BEGIN + +@interface GNCBLEL2CAPClient (Testing) + +/** + * Creates a L2CAP client with a provided peripheral. + * + * This is only exposed for testing and can be used to inject a fake peripheral. + * + * @param peripheral The peripheral instance. + * @param queue The queue to run on, this must match the queue that the peripheral's delegate is + * running on. Defaults to the main queue when @c nil. + * @param requestDisconnectionHandler Called on a private queue with @c peripheral when the + * connection to the peripheral should be cancelled. + */ +-(instancetype)initWithPeripheral:(id)peripheral + queue:(nullable dispatch_queue_t)queue + requestDisconnectionHandler:(GNCRequestDisconnectionHandler)requestDisconnectionHandler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Tests/GNCBLEL2CAPClientTest.m b/internal/platform/implementation/apple/Tests/GNCBLEL2CAPClientTest.m index b3f56351..a53e5a2f 100644 --- a/internal/platform/implementation/apple/Tests/GNCBLEL2CAPClientTest.m +++ b/internal/platform/implementation/apple/Tests/GNCBLEL2CAPClientTest.m @@ -19,30 +19,75 @@ #import #import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h" +#import "internal/platform/implementation/apple/Tests/GNCBLEL2CAPClient+Testing.h" #import "internal/platform/implementation/apple/Tests/GNCFakePeripheral.h" @interface GNCBLEL2CAPClientTest : XCTestCase +@property(nonatomic) GNCFakePeripheral *fakePeripheral; +@property(nonatomic) GNCBLEL2CAPClient *l2capClient; +@property(nonatomic) XCTestExpectation *requestDisconnectionHandlerExpectation; @end @implementation GNCBLEL2CAPClientTest #pragma mark - Tests -- (void)testDisconnect { - GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; - XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Disconnect."]; - - GNCBLEL2CAPClient *l2capClient = - [[GNCBLEL2CAPClient alloc] initWithPeripheral:fakePeripheral +- (void)setUp { + [super setUp]; + self.fakePeripheral = [[GNCFakePeripheral alloc] init]; + self.l2capClient = + [[GNCBLEL2CAPClient alloc] initWithPeripheral:self.fakePeripheral + queue:nil requestDisconnectionHandler:^(id _Nonnull peripheral) { XCTAssertNotNil(peripheral); - [expectation fulfill]; + [self.requestDisconnectionHandlerExpectation fulfill]; }]; +} - [l2capClient disconnect]; +- (void)tearDown { + self.fakePeripheral = nil; + self.l2capClient = nil; + [super tearDown]; +} - [self waitForExpectations:@[ expectation ] timeout:3]; +- (void)testSuccessfulOpenL2CAPChannel { + XCTestExpectation *expectation = + [self expectationWithDescription:@"L2CAP channel opened successfully"]; + uint16_t psm = 123; + + [self.l2capClient openL2CAPChannelWithPSM:psm + completionHandler:^(GNCBLEL2CAPStream *stream, NSError *error) { + XCTAssertNil(error, @"Error should be nil"); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:1.0]; +} + +- (void)testOpenL2CAPChannelWithError { + XCTestExpectation *expectation = + [self expectationWithDescription:@"L2CAP channel open failed with error"]; + uint16_t psm = 123; + NSError *expectedError = [NSError errorWithDomain:@"TestErrorDomain" code:1 userInfo:nil]; + self.fakePeripheral.openL2CAPChannelError = expectedError; + + [self.l2capClient openL2CAPChannelWithPSM:psm + completionHandler:^(GNCBLEL2CAPStream *stream, NSError *error) { + XCTAssertNotNil(error, @"Error should not be nil"); + XCTAssertEqualObjects(error, expectedError); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:1.0]; +} + +- (void)testDisconnect { + self.requestDisconnectionHandlerExpectation = + [self expectationWithDescription:@"Request disconnection handler should be called"]; + + [self.l2capClient disconnect]; + + [self waitForExpectations:@[ self.requestDisconnectionHandlerExpectation ] timeout:1.0]; } @end - diff --git a/internal/platform/implementation/apple/Tests/GNCFakePeripheral.m b/internal/platform/implementation/apple/Tests/GNCFakePeripheral.m index b421e455..abd88135 100644 --- a/internal/platform/implementation/apple/Tests/GNCFakePeripheral.m +++ b/internal/platform/implementation/apple/Tests/GNCFakePeripheral.m @@ -108,6 +108,19 @@ NS_ASSUME_NONNULL_BEGIN }]; } +- (void)openL2CAPChannel:(CBL2CAPPSM)PSM { + [self delayDelegateUsingBlock:^() { + CBL2CAPChannel *channel = [[CBL2CAPChannel alloc] init]; + if (_openL2CAPChannelError) { + [peripheralDelegate gnc_peripheral:self + didOpenL2CAPChannel:channel + error:_openL2CAPChannelError]; + } else { + [peripheralDelegate gnc_peripheral:self didOpenL2CAPChannel:channel error:nil]; + } + }]; +} + - (void)delayDelegateUsingBlock:(void (^)())block { if (_delegateDelay <= 0) { block(); @@ -119,18 +132,6 @@ NS_ASSUME_NONNULL_BEGIN } } -- (void)openL2CAPChannelWithPSM:(uint16_t)PSM { - [self delayDelegateUsingBlock:^() { - if (_openL2CAPChannelError) { - CBL2CAPChannel *channel = [[CBL2CAPChannel alloc] init]; - [peripheralDelegate gnc_peripheral:self - didOpenL2CAPChannel:channel - error:_openL2CAPChannelError]; - } - // TODO: b/399815436 - Add testing for L2CAP channels if error is nil. - }]; -} - @end NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/ble_medium.mm b/internal/platform/implementation/apple/ble_medium.mm index 0baf2963..f4a6c99b 100644 --- a/internal/platform/implementation/apple/ble_medium.mm +++ b/internal/platform/implementation/apple/ble_medium.mm @@ -491,11 +491,30 @@ std::unique_ptr BleMedium::ConnectOverL2cap( return nullptr; } - // TODO: b/399815436 - Continue to add implementation for this method when BleL2capSocket is - // ready. - [medium_ openL2CAPChannelWithPSM:psm peripheral:non_empty_peripheral->GetPeripheral()]; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block std::unique_ptr socket; + std::string service_id_str = service_id; + [medium_ openL2CAPChannelWithPSM:psm + peripheral:non_empty_peripheral->GetPeripheral() + completionHandler:^(GNCBLEL2CAPStream *stream, NSError *error) { + if (error) { + dispatch_semaphore_signal(semaphore); + return; + } + GNCBLEL2CAPConnection *connection = + [GNCBLEL2CAPConnection connectionWithStream:stream + serviceID:@(service_id_str.c_str()) + incomingConnection:NO + callbackQueue:dispatch_get_main_queue()]; + socket = std::make_unique(connection, non_empty_peripheral); + dispatch_semaphore_signal(semaphore); + }]; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + if (socket == nullptr) { + return nullptr; + } - return nullptr; + return std::move(socket); } bool BleMedium::IsExtendedAdvertisementsAvailable() {