feat: add GNCBLEL2CAPClient

PiperOrigin-RevId: 749085219
This commit is contained in:
Edwin Wu
2025-04-18 11:04:50 -07:00
committed by Copybara-Service
parent dba354f5b1
commit 24b8cff1a7
7 changed files with 220 additions and 3 deletions
@@ -285,7 +285,9 @@ static NSError *AlreadyReadingCharacteristicError() {
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error {
dispatch_async(_queue, ^{
[self gnc_peripheral:peripheral didDiscoverServices:error];
if ([self respondsToSelector:@selector(gnc_peripheral:didDiscoverServices:)]) {
[self gnc_peripheral:peripheral didDiscoverServices:error];
}
});
}
@@ -293,7 +295,10 @@ static NSError *AlreadyReadingCharacteristicError() {
didDiscoverCharacteristicsForService:(CBService *)service
error:(nullable NSError *)error {
dispatch_async(_queue, ^{
[self gnc_peripheral:peripheral didDiscoverCharacteristicsForService:service error:error];
if ([self respondsToSelector:@selector(gnc_peripheral:
didDiscoverCharacteristicsForService:error:)]) {
[self gnc_peripheral:peripheral didDiscoverCharacteristicsForService:service error:error];
}
});
}
@@ -301,7 +306,10 @@ static NSError *AlreadyReadingCharacteristicError() {
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(nullable NSError *)error {
dispatch_async(_queue, ^{
[self gnc_peripheral:peripheral didUpdateValueForCharacteristic:characteristic error:error];
if ([self respondsToSelector:@selector(gnc_peripheral:
didUpdateValueForCharacteristic:error:)]) {
[self gnc_peripheral:peripheral didUpdateValueForCharacteristic:characteristic error:error];
}
});
}
@@ -0,0 +1,53 @@
// 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 <Foundation/Foundation.h>
@protocol GNCPeripheral;
NS_ASSUME_NONNULL_BEGIN
/**
* A block to be invoked after a call to @c disconnect, requesting that the local connection to the
* remote peripheral be cancelled.
*
* @param peripheral The remote peripheral to disconnect from.
*/
typedef void (^GNCRequestDisconnectionHandler)(id<GNCPeripheral> peripheral);
/**
* An object that can be used to connect BLE over L2CAP on a remote peripheral.
*
* @note The public APIs of this class are thread safe.
*/
@interface GNCBLEL2CAPClient : NSObject
- (instancetype)init NS_UNAVAILABLE;
/**
* Initializes the L2CAP client with a specified peripheral.
*
* @param peripheral The peripheral instance.
* @param requestDisconnectionHandler Called on a private queue with @c peripheral when the
* connection to the peripheral should be cancelled.
*/
- (instancetype)initWithPeripheral:(id<GNCPeripheral>)peripheral
requestDisconnectionHandler:(GNCRequestDisconnectionHandler)requestDisconnectionHandler;
/** Cancels an active or pending local connection to a peripheral. */
- (void)disconnect;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,91 @@
// 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 <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEError.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h"
#import "GoogleToolboxForMac/GTMLogger.h"
static char *const kGNCBLEL2CAPClientQueueLabel = "com.googlenearby.GNCBLEL2CAPClient";
@interface GNCBLEL2CAPClient () <GNCPeripheralDelegate>
@end
@implementation GNCBLEL2CAPClient {
dispatch_queue_t _queue;
id<GNCPeripheral> _peripheral;
GNCRequestDisconnectionHandler _requestDisconnectionHandler;
// The L2CAP channel that is used to send and receive data.
CBL2CAPChannel *_channel;
}
- (instancetype)initWithPeripheral:(id<GNCPeripheral>)peripheral
requestDisconnectionHandler:(GNCRequestDisconnectionHandler)requestDisconnectionHandler {
return [self initWithPeripheral:peripheral
queue:dispatch_queue_create(kGNCBLEL2CAPClientQueueLabel,
DISPATCH_QUEUE_SERIAL)
requestDisconnectionHandler:requestDisconnectionHandler];
};
// This is private and should only be used for tests. The provided peripheral must call
// delegate methods on the main queue.
- (instancetype)initWithPeripheral:(id<GNCPeripheral>)peripheral
queue:(nullable dispatch_queue_t)queue
requestDisconnectionHandler:(GNCRequestDisconnectionHandler)requestDisconnectionHandler {
self = [super init];
if (self) {
_queue = queue ?: dispatch_get_main_queue();
_peripheral = peripheral;
_peripheral.peripheralDelegate = self;
_requestDisconnectionHandler = requestDisconnectionHandler;
}
return self;
};
- (void)disconnect {
dispatch_async(_queue, ^{
_requestDisconnectionHandler(_peripheral);
});
}
#pragma mark - GNCPeripheralDelegate
- (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);
return;
}
GTMLoggerInfo(@"[NEARBY] Opened L2CAP channel: %@", channel);
_channel = channel;
// TODO: b/399815436 - Implement to wrap up l2cap channel.
}
#pragma mark - CBPeripheralDelegate
- (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];
}
}
@end
@@ -131,6 +131,7 @@ NS_ASSUME_NONNULL_BEGIN
* @param peripheral The peripheral to which the services belong.
* @param error The reason the call failed, or @c nil if no error occurred.
*/
@optional
- (void)gnc_peripheral:(id<GNCPeripheral>)peripheral didDiscoverServices:(nullable NSError *)error;
/**
@@ -164,6 +165,19 @@ NS_ASSUME_NONNULL_BEGIN
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(nullable NSError *)error;
/**
* Tells the delegate that the peripheral opened an L2CAP channel.
*
* Called when your app calls the @c openL2CAPChannel: method. If successful, the @c error parameter
* is @c nil. If unsuccessful, the @c error parameter returns the cause of the failure.
*
* @param peripheral The peripheral providing this information.
* @param channel The L2CAP channel that was opened.
* @param error The reason the call failed, or @c nil if no error occurred.
*/
- (void)gnc_peripheral:(id<GNCPeripheral>)peripheral
didOpenL2CAPChannel:(CBL2CAPChannel *)characteristic
error:(nullable NSError *)error;
@end
/**
@@ -25,6 +25,7 @@ objc_library(
"BLEv2/GNCBLEGATTCharacteristic.m",
"BLEv2/GNCBLEGATTClient.m",
"BLEv2/GNCBLEGATTServer.m",
"BLEv2/GNCBLEL2CAPClient.m",
"BLEv2/GNCBLEL2CAPServer.m",
"BLEv2/GNCBLEL2CAPStream.m",
"BLEv2/GNCBLEMedium.m",
@@ -52,6 +53,7 @@ objc_library(
"BLEv2/GNCBLEGATTCharacteristic.h",
"BLEv2/GNCBLEGATTClient.h",
"BLEv2/GNCBLEGATTServer.h",
"BLEv2/GNCBLEL2CAPClient.h",
"BLEv2/GNCBLEL2CAPServer.h",
"BLEv2/GNCBLEL2CAPStream.h",
"BLEv2/GNCBLEMedium.h",
@@ -28,6 +28,7 @@ objc_library(
"GNCBLEGATTClientTest.m",
"GNCBLEGATTServer+Testing.h",
"GNCBLEGATTServerTest.m",
"GNCBLEL2CAPClientTest.m",
"GNCBLEL2CAPFakeInputOutputStream.h",
"GNCBLEL2CAPFakeInputOutputStream.m",
"GNCBLEL2CAPServerTest.m",
@@ -0,0 +1,48 @@
// 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 <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h"
#import "internal/platform/implementation/apple/Tests/GNCFakePeripheral.h"
@interface GNCBLEL2CAPClientTest : XCTestCase
@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
requestDisconnectionHandler:^(id<GNCPeripheral> _Nonnull peripheral) {
XCTAssertNotNil(peripheral);
[expectation fulfill];
}];
[l2capClient disconnect];
[self waitForExpectations:@[ expectation ] timeout:3];
}
@end