Implement L2CAPServer

PiperOrigin-RevId: 733593766
This commit is contained in:
Edwin Wu
2025-03-04 22:50:09 -08:00
committed by Copybara-Service
parent b6dc3348a4
commit 18f52de400
9 changed files with 410 additions and 0 deletions
@@ -0,0 +1,39 @@
// 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 <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
* An object that publishes the @c PSM value.
*
* @note The public APIs of this class are thread safe.
*/
@interface GNCBLEL2CAPServer : NSObject
// Represents a PSM (Protocol/Service Multiplexer) value for an L2CAP channel.
@property(atomic, readonly) CBL2CAPPSM PSM;
/**
* Retrieves the L2CAP channel.
*
* @return The L2CAP channel, or nil if the channel is not available.
*/
- (nullable CBL2CAPChannel *)getChannel;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,158 @@
// 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/GNCBLEL2CAPServer.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/GNCPeripheralManager.h"
#import "GoogleToolboxForMac/GTMLogger.h"
NS_ASSUME_NONNULL_BEGIN
static char *const kGNCBLEL2CAPServerQueueLabel = "com.nearby.GNCBLEL2CAPServer";
@interface GNCBLEL2CAPServer () <GNCPeripheralManagerDelegate>
@property(atomic, readwrite) CBL2CAPPSM PSM;
@end
@implementation GNCBLEL2CAPServer {
dispatch_queue_t _queue;
id<GNCPeripheralManager> _peripheralManager;
// The L2CAP channel that is used to send and receive data.
CBL2CAPChannel *_channel;
}
- (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;
}
// 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<GNCPeripheralManager>)peripheralManager {
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;
}
return self;
}
- (nullable CBL2CAPChannel *)getChannel {
return _channel;
}
#pragma mark - GNCPeripheralManagerDelegate
- (void)gnc_peripheralManagerDidUpdateState:(id<GNCPeripheralManager>)peripheral {
dispatch_assert_queue(_queue);
if (_peripheralManager.state == CBManagerStatePoweredOn) {
[_peripheralManager publishL2CAPChannelWithEncryption:NO];
}
if (_peripheralManager.state == CBManagerStatePoweredOff) {
[_peripheralManager unpublishL2CAPChannel:_PSM];
}
}
- (void)gnc_peripheralManager:(id<GNCPeripheralManager>)peripheral
didPublishL2CAPChannel:(CBL2CAPPSM)PSM
error:(nullable NSError *)error {
dispatch_assert_queue(_queue);
if (error) {
GTMLoggerError(@"Failed to publish L2CAP channel: %@", error);
}
GTMLoggerInfo(@"Published L2CAP channel with PSM: %d", PSM);
_PSM = PSM;
}
- (void)gnc_peripheralManager:(id<GNCPeripheralManager>)peripheral
didUnpublishL2CAPChannel:(CBL2CAPPSM)PSM
error:(NSError *)error {
dispatch_assert_queue(_queue);
if (error) {
GTMLoggerError(@"Failed to unpublish L2CAP channel: %@", error);
}
_PSM = 0;
}
- (void)gnc_peripheralManager:(id<GNCPeripheralManager>)peripheral
didOpenL2CAPChannel:(nullable CBL2CAPChannel *)channel
error:(nullable NSError *)error {
dispatch_assert_queue(_queue);
if (error) {
GTMLoggerError(@"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.
}
#pragma mark - CBPeripheralManagerDelegate
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
dispatch_async(_queue, ^{
[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];
});
}
- (void)peripheralManager:(CBPeripheralManager *)peripheral
didUnpublishL2CAPChannel:(CBL2CAPPSM)PSM
error:(nullable NSError *)error {
dispatch_async(_queue, ^{
[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];
});
}
#pragma mark Private
- (void)tearDown {
if (_PSM > 0) {
[_peripheralManager unpublishL2CAPChannel:_PSM];
}
_peripheralManager.peripheralDelegate = nil;
_peripheralManager = nil;
}
@end
NS_ASSUME_NONNULL_END
@@ -118,6 +118,20 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)stopAdvertising;
/**
* Publishes an L2CAP channel with optional encryption.
*
* @param encryptionRequired A boolean value indicating whether encryption is required for the L2CAP channel.
*/
- (void)publishL2CAPChannelWithEncryption:(BOOL)encryptionRequired;
/**
* Unpublishes an L2CAP channel.
*
* @param PSM The @c PSM (Protocol/Service Multiplexer) value assigned to the published channel.
*/
- (void)unpublishL2CAPChannel:(CBL2CAPPSM)PSM;
@end
/**
@@ -156,6 +170,7 @@ NS_ASSUME_NONNULL_BEGIN
* @param peripheral The peripheral manager that is starting advertising.
* @param error The reason the call failed, or @c nil if no error occurred.
*/
@optional
- (void)gnc_peripheralManagerDidStartAdvertising:(id<GNCPeripheralManager>)peripheral
error:(nullable NSError *)error;
@@ -171,6 +186,7 @@ NS_ASSUME_NONNULL_BEGIN
* @param service The service added to the local GATT database.
* @param error The reason the call failed, or @c nil if no error occurred.
*/
@optional
- (void)gnc_peripheralManager:(id<GNCPeripheralManager>)peripheral
didAddService:(CBService *)service
error:(nullable NSError *)error;
@@ -186,9 +202,49 @@ NS_ASSUME_NONNULL_BEGIN
* @param request A @c CBATTRequest object that represents a request to read a characteristics
* value.
*/
@optional
- (void)gnc_peripheralManager:(id<GNCPeripheralManager>)peripheral
didReceiveReadRequest:(CBATTRequest *)request;
/**
* Called when an L2CAP channel has been published by the peripheral.
*
* @param peripheral The peripheral that published the L2CAP channel.
* @param PSM The @c PSM (Protocol/Service Multiplexer) value assigned to the published channel.
* @param error An error object indicating why the channel failed to publish, or @c nil if
* successful.
*/
@optional
- (void)gnc_peripheralManager:(id<GNCPeripheralManager>)peripheral
didPublishL2CAPChannel:(CBL2CAPPSM)PSM
error:(nullable NSError *)error;
/**
* Called when an L2CAP channel has been unpublished by the peripheral.
*
* @param peripheral The peripheral that unpublished the L2CAP channel.
* @param PSM The @c PSM (Protocol/Service Multiplexer) value assigned to the unpublished channel.
* @param error An error object indicating why the channel failed to unpublish, or @c nil if
* successful.
*/
@optional
- (void)gnc_peripheralManager:(id<GNCPeripheralManager>)peripheral
didUnpublishL2CAPChannel:(CBL2CAPPSM)PSM
error:(NSError *)error;
/**
* Called when an L2CAP channel has been opened by the peripheral.
*
* @param peripheral The peripheral that opened the L2CAP channel.
* @param channel The opened L2CAP channel, or nil if an error occurred.
* @param error An error object indicating why the channel failed to
* open, or @c nil if successful.
*/
@optional
- (void)gnc_peripheralManager:(id<GNCPeripheralManager>)peripheral
didOpenL2CAPChannel:(nullable CBL2CAPChannel *)channel
error:(nullable NSError *)error;
@end
@interface CBPeripheralManager () <GNCPeripheralManager>
@@ -25,6 +25,7 @@ objc_library(
"BLEv2/GNCBLEGATTCharacteristic.m",
"BLEv2/GNCBLEGATTClient.m",
"BLEv2/GNCBLEGATTServer.m",
"BLEv2/GNCBLEL2CAPServer.m",
"BLEv2/GNCBLEMedium.m",
"BLEv2/GNCCentralManager.m",
"BLEv2/GNCPeripheral.m",
@@ -48,6 +49,7 @@ objc_library(
"BLEv2/GNCBLEGATTCharacteristic.h",
"BLEv2/GNCBLEGATTClient.h",
"BLEv2/GNCBLEGATTServer.h",
"BLEv2/GNCBLEL2CAPServer.h",
"BLEv2/GNCBLEMedium.h",
"BLEv2/GNCCentralManager.h",
"BLEv2/GNCPeripheral.h",
@@ -28,6 +28,8 @@ objc_library(
"GNCBLEGATTClientTest.m",
"GNCBLEGATTServer+Testing.h",
"GNCBLEGATTServerTest.m",
"GNCBLEL2CAPServer+Testing.h",
"GNCBLEL2CAPServerTest.m",
"GNCBLEMedium+Testing.h",
"GNCBLEMediumTest.m",
"GNCBLEUtilsTest.mm",
@@ -0,0 +1,36 @@
// 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/GNCBLEL2CAPServer.h"
#import <Foundation/Foundation.h>
@protocol GNCPeripheralManager;
NS_ASSUME_NONNULL_BEGIN
@interface GNCBLEL2CAPServer (Testing)
/**
* Creates a L2CAP server with a provided peripheral manager.
*
* This is only exposed for testing and can be used to inject a fake peripheral manager.
*
* @param peripheralManager The peripheral manager instance.
*/
- (instancetype)initWithPeripheralManager:(id<GNCPeripheralManager>)peripheralManager;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,75 @@
// 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/GNCBLEL2CAPServer.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#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
- (void)testPublishL2CAPChannel {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
fakePeripheralManager.PSM = kPSM;
GNCBLEL2CAPServer *l2capServer =
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
XCTAssertEqual([l2capServer PSM], kPSM);
XCTAssertNotNil([l2capServer getChannel]);
}
- (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];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
XCTAssertNotEqual([l2capServer PSM], kPSM);
XCTAssertNil([l2capServer getChannel]);
}
- (void)testUnPublishL2CAPChannel {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
fakePeripheralManager.PSM = kPSM;
GNCBLEL2CAPServer *l2capServer =
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOff];
XCTAssertEqual([l2capServer PSM], 0);
}
@end
@@ -35,6 +35,9 @@ NS_ASSUME_NONNULL_BEGIN
/** The data being advertised. */
@property(nonatomic, nullable, readonly) NSDictionary<NSString *, id> *advertisementData;
/** The PSM of the L2CAP channel. */
@property(nonatomic) CBL2CAPPSM PSM;
/** Expectation fulfilled when peripheral responds to a request with success. */
@property(nonatomic, readonly) XCTestExpectation *respondToRequestSuccessExpectation;
@@ -59,6 +62,24 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property(nonatomic, nullable, readwrite) NSError *didStartAdvertisingError;
/**
* Similates a @c publishL2CAPChannel: error.
*
* Setting this error to a value other than @c nil will simulate a failure when calling @c
* publishL2CAPChannel: and will call the @c gnc_peripheralManager:didPublishL2CAPChannel:error:
* delegate method with the provided error.
*/
@property(nonatomic, nullable, readwrite) NSError *didPublishL2CAPChannelError;
/**
* Similates a @c unpublishL2CAPChannel: error.
*
* Setting this error to a value other than @c nil will simulate a failure when calling @c
* unpublishL2CAPChannel: and will call the @c gnc_peripheralManager:didUnpublishL2CAPChannel:error:
* delegate method with the provided error.
*/
@property(nonatomic, nullable, readwrite) NSError *didUnPublishL2CAPChannelError;
/**
* Simulates a state update event.
*
@@ -122,6 +122,27 @@
_isAdvertising = false;
}
- (void)publishL2CAPChannelWithEncryption:(BOOL)encryptionRequired {
CBL2CAPPSM localPSM = 0;
if (!_didPublishL2CAPChannelError) {
localPSM = _PSM;
}
[peripheralDelegate gnc_peripheralManager:self
didPublishL2CAPChannel:localPSM
error:_didPublishL2CAPChannelError];
if (!_didPublishL2CAPChannelError) {
[peripheralDelegate gnc_peripheralManager:self
didOpenL2CAPChannel:[[CBL2CAPChannel alloc] init]
error:nil];
}
}
- (void)unpublishL2CAPChannel:(CBL2CAPPSM)PSM {
[peripheralDelegate gnc_peripheralManager:self
didUnpublishL2CAPChannel:_PSM
error:_didUnPublishL2CAPChannelError];
}
#pragma mark - Testing Helpers
- (NSArray<CBService *> *)services {