Add completion handler to opened L2CAP channel for iOS client.

PiperOrigin-RevId: 753363177
This commit is contained in:
Edwin Wu
2025-04-30 16:49:59 -07:00
committed by Copybara-Service
parent 541bffc858
commit fc0768f2c4
11 changed files with 228 additions and 49 deletions
@@ -14,10 +14,16 @@
#import <Foundation/Foundation.h>
#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<GNCPeripheral> 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;
@@ -18,10 +18,11 @@
#import <Foundation/Foundation.h>
#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 () <GNCPeripheralDelegate>
@end
@@ -29,10 +30,11 @@ static char *const kGNCBLEL2CAPClientQueueLabel = "com.googlenearby.GNCBLEL2CAPC
@implementation GNCBLEL2CAPClient {
dispatch_queue_t _queue;
id<GNCPeripheral> _peripheral;
GNCOpenL2CAPStreamCompletionHandler _completionHandler;
GNCRequestDisconnectionHandler _requestDisconnectionHandler;
// The L2CAP channel that is used to send and receive data.
CBL2CAPChannel *_l2CAPChannel;
GNCBLEL2CAPStream *_l2CAPStream;
}
- (instancetype)initWithPeripheral:(id<GNCPeripheral>)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<GNCPeripheral>)peripheral didOpenL2CAPChannel:(CBL2CAPChannel *)channel
error:(NSError *)error {
- (void)gnc_peripheral:(id<GNCPeripheral>)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
@@ -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(
@@ -15,8 +15,9 @@
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.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/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<GNCPeripheral>)remotePeripheral;
- (void)openL2CAPChannelWithPSM:(uint16_t)PSM
peripheral:(id<GNCPeripheral>)remotePeripheral
completionHandler:(nullable GNCOpenL2CAPStreamCompletionHandler)completionHandler;
@end
@@ -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<GNCPeripheral>)remotePeripheral {
- (void)openL2CAPChannelWithPSM:(uint16_t)psm
peripheral:(id<GNCPeripheral>)remotePeripheral
completionHandler:(nullable GNCOpenL2CAPStreamCompletionHandler)completionHandler {
dispatch_async(_queue, ^{
if (!_l2capClient) {
_l2capClient =
@@ -229,7 +231,16 @@ static GNCBLEL2CAPServer *_Nonnull CreateL2CapServer(
requestDisconnectionHandler:^(id<GNCPeripheral> _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);
}
}];
});
}
@@ -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
@@ -28,6 +28,7 @@ objc_library(
"GNCBLEGATTClientTest.m",
"GNCBLEGATTServer+Testing.h",
"GNCBLEGATTServerTest.m",
"GNCBLEL2CAPClient+Testing.h",
"GNCBLEL2CAPClientTest.m",
"GNCBLEL2CAPConnectionTest.m",
"GNCBLEL2CAPFakeInputOutputStream.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 <Foundation/Foundation.h>
@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<GNCPeripheral>)peripheral
queue:(nullable dispatch_queue_t)queue
requestDisconnectionHandler:(GNCRequestDisconnectionHandler)requestDisconnectionHandler;
@end
NS_ASSUME_NONNULL_END
@@ -19,30 +19,75 @@
#import <XCTest/XCTest.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/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<GNCPeripheral> _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
@@ -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
@@ -491,11 +491,30 @@ std::unique_ptr<api::ble_v2::BleL2capSocket> 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<BleL2capSocket> 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<BleL2capSocket>(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() {