Add GNCBLEL2CAPConnection

PiperOrigin-RevId: 750016094
This commit is contained in:
Edwin Wu
2025-04-21 20:08:48 -07:00
committed by Copybara-Service
parent adb38095d8
commit af148603b6
5 changed files with 237 additions and 0 deletions
@@ -0,0 +1,52 @@
// 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/GNCMConnection.h"
NS_ASSUME_NONNULL_BEGIN
@class GNCBLEL2CAPStream;
/**
* A GNCBLEL2CAPConnection implementation for BLE L2CAP.
* This class is thread-safe.
*/
@interface GNCBLEL2CAPConnection : NSObject
@property(nonatomic) GNCMConnectionHandlers *connectionHandlers;
/*
* Creates a |GNCBLEL2CAPConnection|.
*
* @param stream A |GNCL2CAPStream| instance.
* @param serviceID A string that uniquely identifies the service.
* @param incomingConnection A flag to indicate the connection is incoming.
* @param callbackQueue The queue on which all callbacks are made.
*/
+ (instancetype)connectionWithStream:(GNCBLEL2CAPStream *)stream
serviceID:(nullable NSString *)serviceID
incomingConnection:(BOOL)incomingConnection
callbackQueue:(dispatch_queue_t)callbackQueue;
/**
* Sends data to the remote device.
*
* @param payload The data to send.
* @param completion A block that is called when the data has been sent. The block takes a BOOL
* parameter indicating whether the data was sent successfully.
*/
- (void)sendData:(NSData *)payload completion:(void (^)(BOOL))completion;
@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/GNCBLEL2CAPConnection.h"
#import <Foundation/Foundation.h>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.h"
#import "internal/platform/implementation/apple/Mediums/Ble/GNCMBleUtils.h"
#import "internal/platform/implementation/apple/Mediums/GNCLeaks.h"
#import "internal/platform/implementation/apple/Mediums/GNCMConnection.h"
#import "GoogleToolboxForMac/GTMLogger.h"
enum { kL2CAPPacketLength = 4 };
static char *const kGNCBLEL2CAPConnectionQueueLabel = "com.google.nearby.GNCBLEL2CAPConnection";
static NSData *PrefixDataWithServiceIDHash(NSData *serviceIDHash, NSData *data) {
NSMutableData *combinedData = [NSMutableData dataWithCapacity:serviceIDHash.length + data.length];
[combinedData appendData:serviceIDHash];
[combinedData appendData:data];
return combinedData;
}
static NSData *PrefixLengthData(NSData *data) {
uint32_t dataLength = (uint32_t)data.length;
uint32_t lengthBigEndian = CFSwapInt32HostToBig(dataLength);
NSMutableData *packet = [NSMutableData dataWithCapacity:sizeof(uint32_t)];
[packet appendBytes:&lengthBigEndian length:sizeof(uint32_t)];
[packet appendData:data];
return packet;
}
@interface GNCBLEL2CAPConnection () <GNCBLEL2CAPStreamDelegate>
@property(nonatomic) dispatch_queue_t selfQueue;
@property(nonatomic) GNCBLEL2CAPStream *stream;
@property(nonatomic) NSData *serviceIDHash;
@property(nonatomic) dispatch_queue_t callbackQueue;
@property(nonatomic) BOOL incomingConnection;
@end
@implementation GNCBLEL2CAPConnection
+ (instancetype)connectionWithStream:(GNCBLEL2CAPStream *)stream
serviceID:(NSString *)serviceID
incomingConnection:(BOOL)incomingConnection
callbackQueue:(dispatch_queue_t)callbackQueue {
GNCBLEL2CAPConnection *connection = [[GNCBLEL2CAPConnection alloc] init];
[connection setStream:stream];
stream.delegate = connection;
[connection setServiceIDHash:GNCMServiceIDHash(serviceID)];
[connection setCallbackQueue:callbackQueue];
[connection
setSelfQueue:dispatch_queue_create(kGNCBLEL2CAPConnectionQueueLabel,
dispatch_queue_attr_make_with_qos_class(
DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1))];
[connection setIncomingConnection:incomingConnection];
return connection;
}
- (void)dealloc {
[_stream close];
GNCVerifyDealloc(_stream, 2); // assert if not deallocated
}
#pragma mark GNCBLEL2CAPConnection
- (void)sendData:(NSData *)data completion:(void (^)(BOOL))completion {
dispatch_async(_selfQueue, ^{
NSData *packet;
// Prefix the service ID hash.
packet = PrefixLengthData(PrefixDataWithServiceIDHash(_serviceIDHash, data));
GTMLoggerInfo(@"[NEARBY] GNCBLEL2CAPConnection data to be sent: %@", [packet description]);
[_stream sendData:packet
completionBlock:^(BOOL result) {
dispatch_async(_callbackQueue, ^{
completion(result);
});
}];
});
}
#pragma mark GNCBLEL2CAPStreamDelegate
- (void)stream:(GNCBLEL2CAPStream *)stream didReceiveData:(NSData *)data {
GTMLoggerDebug(@"[NEARBY] BLEL2CAPConnection didReceiveData, data: %@", [data description]);
NSData *realData = [self extractRealDataFromData:data];
if (realData == nil) {
return;
}
// Validate the L2CAP packet.
// TODO: b/399815436 - Refactor the validation logic to connections layer.
// Extract the service ID prefix from each data packet and validate it.
NSUInteger prefixLength = _serviceIDHash.length;
if (![[realData subdataWithRange:NSMakeRange(0, prefixLength)] isEqual:_serviceIDHash]) {
GTMLoggerInfo(@"[NEARBY]: Received wrong data packet and discarded");
return;
}
dispatch_async(_selfQueue, ^{
if (_connectionHandlers.payloadHandler) {
dispatch_async(_callbackQueue, ^{
_connectionHandlers.payloadHandler([NSData
dataWithData:[realData subdataWithRange:NSMakeRange(prefixLength,
realData.length - prefixLength)]]);
});
}
});
}
- (void)stream:(GNCBLEL2CAPStream *)stream didDisconnectWithError:(NSError *_Nullable)error {
dispatch_async(_selfQueue, ^{
if (_connectionHandlers.disconnectedHandler) {
dispatch_async(_callbackQueue, ^{
_connectionHandlers.disconnectedHandler();
});
}
});
}
#pragma mark Private
- (NSData *_Nullable)extractRealDataFromData:(NSData *)data {
if (data.length < kL2CAPPacketLength) {
GTMLoggerError(@"[NEARBY] Packet length mismatch. Expected: > %d, Actual: %lu",
kL2CAPPacketLength, data.length);
return nil;
}
int realDataLength = CFSwapInt32BigToHost(
*(int *)([[data subdataWithRange:NSMakeRange(0, kL2CAPPacketLength)] bytes]));
if (realDataLength != (data.length - kL2CAPPacketLength)) {
GTMLoggerError(@"[NEARBY] Data length mismatch. Expected: %d, Actual: %lu",
realDataLength, data.length - kL2CAPPacketLength);
return nil;
}
return [data subdataWithRange:NSMakeRange(kL2CAPPacketLength, data.length - kL2CAPPacketLength)];
}
@end
@@ -14,6 +14,8 @@
#import <Foundation/Foundation.h>
@class GNCBLEL2CAPStream;
NS_ASSUME_NONNULL_BEGIN
/// Block invoked when the stream is closed.
@@ -22,6 +24,23 @@ typedef void (^GNCBLEL2CAPStreamClosedBlock)(void);
/// Block invoked when |data| is received from the remote device on the L2CAP connection.
typedef void (^GNCBLEL2CAPControllerReceivedDataBlock)(NSData *data);
/** Delegate for @c GNCBLEL2CAPStream. */
@protocol GNCBLEL2CAPStreamDelegate <NSObject>
/**
* Called when data is received from the remote device on the L2CAP connection.
*
* @param data The data received from the remote device.
*/
- (void)stream:(GNCBLEL2CAPStream *)stream didReceiveData:(NSData *)data;
/**
* Called when the stream is closed.
*/
- (void)stream:(GNCBLEL2CAPStream *)stream didDisconnectWithError:(NSError *_Nullable)error;
@end
/**
* Abstraction to take in two streams returned from L2CAP controller and provide a simplified
* interface to send and receive data from the streams.
@@ -29,6 +48,9 @@ typedef void (^GNCBLEL2CAPControllerReceivedDataBlock)(NSData *data);
*/
@interface GNCBLEL2CAPStream : NSObject
/// Delegate for @c GNCBLEL2CAPStream
@property(nonatomic, weak) id<GNCBLEL2CAPStreamDelegate> delegate;
/// Returns an instance of @c GNCBLEL2CAPStream which sends and receives data on |inputStream|
/// and |outputStream|.
/// Invokes |closedBlock| if stream closed signal is received when reading data.
@@ -320,6 +320,8 @@
}
dispatch_async(_receivedDataQueue, ^{
[_delegate stream:self didReceiveData:data];
// TODO: b/399815436 - Remove below once the delegate is implemented.
self->_receivedDataBlock(data);
});
} else if (bytesRead < 0) {
@@ -328,6 +330,7 @@
GTMLoggerDebug(@"[NEARBY] End of stream reached. Disconnecting");
// This indicates the L2CAP socket is closed. Notifying the owner so that it can tear down this
// stream and update its own state.
[_delegate stream:self didDisconnectWithError:nil];
_closedBlock();
}
}
@@ -27,6 +27,7 @@ objc_library(
"BLEv2/GNCBLEGATTClient.m",
"BLEv2/GNCBLEGATTServer.m",
"BLEv2/GNCBLEL2CAPClient.m",
"BLEv2/GNCBLEL2CAPConnection.m",
"BLEv2/GNCBLEL2CAPServer.m",
"BLEv2/GNCBLEL2CAPStream.m",
"BLEv2/GNCBLEMedium.m",
@@ -55,6 +56,7 @@ objc_library(
"BLEv2/GNCBLEGATTClient.h",
"BLEv2/GNCBLEGATTServer.h",
"BLEv2/GNCBLEL2CAPClient.h",
"BLEv2/GNCBLEL2CAPConnection.h",
"BLEv2/GNCBLEL2CAPServer.h",
"BLEv2/GNCBLEL2CAPStream.h",
"BLEv2/GNCBLEMedium.h",