mirror of
https://github.com/kidfromjupiter/nearby.git
synced 2026-09-14 14:46:12 -04:00
Add BLE Medium
PiperOrigin-RevId: 557933813
This commit is contained in:
committed by
Copybara-Service
parent
07a94a950d
commit
7933842c29
@@ -26,4 +26,5 @@ typedef NS_ERROR_ENUM(GNCBLEErrorDomain, GNCBLEError){
|
||||
GNCBLEErrorInvalidCharacteristic,
|
||||
GNCBLEErrorAlreadyDiscoveringSpecifiedCharacteristics,
|
||||
GNCBLEErrorAlreadyReadingCharacteristic,
|
||||
GNCBLEErrorAlreadyScanning,
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ typedef void (^GNCReadCharacteristicValueCompletionHandler)(NSData *_Nullable va
|
||||
*
|
||||
* @param peripheral The peripheral instance.
|
||||
*/
|
||||
- (instancetype)initWithPeripheral:(CBPeripheral *)peripheral;
|
||||
- (instancetype)initWithPeripheral:(id<GNCPeripheral>)peripheral;
|
||||
|
||||
/**
|
||||
* Discovers the specified characteristics of a service.
|
||||
|
||||
@@ -70,7 +70,7 @@ static NSError *AlreadyReadingCharacteristicError() {
|
||||
*_readCharacteristicValueCompletionHandlers;
|
||||
}
|
||||
|
||||
- (instancetype)initWithPeripheral:(CBPeripheral *)peripheral {
|
||||
- (instancetype)initWithPeripheral:(id<GNCPeripheral>)peripheral {
|
||||
return [self
|
||||
initWithPeripheral:peripheral
|
||||
queue:dispatch_queue_create(kGNCBLEGATTClientQueueLabel, DISPATCH_QUEUE_SERIAL)];
|
||||
|
||||
@@ -203,7 +203,7 @@ static char *const kGNCBLEGATTServerQueueLabel = "com.nearby.GNCBLEGATTServer";
|
||||
// data is unavailable.
|
||||
CBUUID *serviceUUID = [serviceData.allKeys objectAtIndex:0];
|
||||
NSData *value = [serviceData objectForKey:serviceUUID];
|
||||
NSString *encoded = [value webSafebase64EncodedString];
|
||||
NSString *encoded = [value webSafeBase64EncodedString];
|
||||
|
||||
// Base64 encoding increases the size of the data so we must truncate it to 22 bytes to ensure
|
||||
// it fits in the advertisement alongside an assumed 16-bit serviceUUID.
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright 2023 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>
|
||||
|
||||
@class GNCBLEGATTServer;
|
||||
@class GNCBLEGATTClient;
|
||||
@class GNCBLEGATTCharacteristic;
|
||||
|
||||
@protocol GNCPeripheral;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* A block to be invoked when a call to @c startAdvertisingData:completionHandler: has completed.
|
||||
*
|
||||
* @param error The cause of the failure, or @c nil if no error occurred.
|
||||
*/
|
||||
typedef void (^GNCStartAdvertisingCompletionHandler)(NSError *_Nullable error);
|
||||
|
||||
/**
|
||||
* A block to be invoked when a peripheral’s advertisement has been discovered.
|
||||
*
|
||||
* @note This block can be called numerous times.
|
||||
*
|
||||
* @param peripheral The discovered peripheral.
|
||||
* @param serviceData A dictionary that contains service-specific advertisement data. The keys
|
||||
* represent services and the values represent the service-specific data.
|
||||
*/
|
||||
typedef void (^GNCAdvertisementFoundHandler)(id<GNCPeripheral> peripheral,
|
||||
NSDictionary<CBUUID *, NSData *> *serviceData);
|
||||
|
||||
/**
|
||||
* A block to be invoked when a call to
|
||||
* @c startScanningForService:advertisementFoundHandler:completionHandler: has completed.
|
||||
*
|
||||
* @param error The cause of the failure, or @c nil if no error occurred.
|
||||
*/
|
||||
typedef void (^GNCStartScanningCompletionHandler)(NSError *_Nullable error);
|
||||
|
||||
/**
|
||||
* A block to be invoked when a call to @c startGATTServerWithCompletionHandler: has completed.
|
||||
*
|
||||
* @param server The successfully started GATT server, or @c nil if an error occurred.
|
||||
* @param error The cause of the failure, or @c nil if no error occurred.
|
||||
*/
|
||||
typedef void (^GNCGATTServerCompletionHandler)(GNCBLEGATTServer *_Nullable server,
|
||||
NSError *_Nullable error);
|
||||
|
||||
/** A block to be invoked when a peripheral has disconnected. */
|
||||
typedef void (^GNCGATTDisconnectionHandler)();
|
||||
|
||||
/**
|
||||
* A block to be invoked when a call to
|
||||
* @c connectToGATTServerForPeripheral:disconnectionHandler:completionHandler: has completed.
|
||||
*
|
||||
* @param client The interface to the remote peripheral’s GATT server, or @c nil if an error
|
||||
* occurred.
|
||||
* @param error The cause of the failure, or @c nil if no error occurred.
|
||||
*/
|
||||
typedef void (^GNCGATTConnectionCompletionHandler)(GNCBLEGATTClient *_Nullable client,
|
||||
NSError *_Nullable error);
|
||||
|
||||
/**
|
||||
* The main BLE medium used inside of Nearby. This serves as the entry point for all BLE and GATT
|
||||
* related operations.
|
||||
*
|
||||
* @note The public APIs of this class are thread safe.
|
||||
*/
|
||||
@interface GNCBLEMedium : NSObject
|
||||
|
||||
/** The hardware supports BOTH advertising extensions and extended scans. */
|
||||
@property(nonatomic, readonly) BOOL supportsExtendedAdvertisements;
|
||||
|
||||
/**
|
||||
* Starts advertising service data in a way that is supported by CoreBluetooth.
|
||||
*
|
||||
* Since CoreBluetooth doesn't support setting the @c CBAdvertisementDataServiceDataKey key, the
|
||||
* service list is advertised using @c CBAdvertisementDataServiceUUIDsKey and the associated data is
|
||||
* advertised using @c CBAdvertisementDataLocalNameKey. Since @c CBAdvertisementDataLocalNameKey
|
||||
* does not support binary data, the value is base64 encoded and truncated if the resulting value is
|
||||
* longer than 22 bytes. This also means we can only support advertising a single service.
|
||||
*
|
||||
* @param serviceData A dictionary that contains service-specific advertisement data.
|
||||
* @param completionHandler Called on a private queue with @c nil if successfully started
|
||||
* advertising or an error if one has occured.
|
||||
*/
|
||||
- (void)startAdvertisingData:(NSDictionary<CBUUID *, NSData *> *)serviceData
|
||||
completionHandler:(nullable GNCStartAdvertisingCompletionHandler)completionHandler;
|
||||
|
||||
/**
|
||||
* Scans for peripherals that are advertising the specified service.
|
||||
*
|
||||
* @param serviceUUID The service UUID to scan for.
|
||||
* @param advertisementFoundHandler Called on a private queue when a peripheral has been discovered.
|
||||
* @param completionHandler Called on a private queue with @c nil if successfully started scanning
|
||||
* or an error if one has occured.
|
||||
*/
|
||||
- (void)startScanningForService:(CBUUID *)serviceUUID
|
||||
advertisementFoundHandler:(GNCAdvertisementFoundHandler)advertisementFoundHandler
|
||||
completionHandler:(nullable GNCStartScanningCompletionHandler)completionHandler;
|
||||
|
||||
/**
|
||||
* Starts a GATT server.
|
||||
*
|
||||
* @param completionHandler Called on a private queue with the GATT server if successfully started
|
||||
* or an error if one has occured.
|
||||
*/
|
||||
- (void)startGATTServerWithCompletionHandler:
|
||||
(nullable GNCGATTServerCompletionHandler)completionHandler;
|
||||
|
||||
/**
|
||||
* Connects to a peripheral’s GATT server.
|
||||
*
|
||||
* @param remotePeripheral The peripheral to which the central is attempting to connect.
|
||||
* @param disconnectionHandler Called on a private queue when the peripheral has been disconnected.
|
||||
* @param completionHandler Called on a private queue with a GATT client if successfully connected
|
||||
* or an error if one has occured.
|
||||
*/
|
||||
- (void)connectToGATTServerForPeripheral:(id<GNCPeripheral>)remotePeripheral
|
||||
disconnectionHandler:(nullable GNCGATTDisconnectionHandler)disconnectionHandler
|
||||
completionHandler:
|
||||
(nullable GNCGATTConnectionCompletionHandler)completionHandler;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,294 @@
|
||||
// Copyright 2023 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/GNCBLEMedium.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/GNCBLEGATTClient.h"
|
||||
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.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+GNCWebSafeBase64.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
static char *const kBLEMediumQueueLabel = "com.nearby.GNCBLEMedium";
|
||||
|
||||
static NSError *AlreadyScanningError() {
|
||||
return [NSError errorWithDomain:GNCBLEErrorDomain code:GNCBLEErrorAlreadyScanning userInfo:nil];
|
||||
}
|
||||
|
||||
@interface GNCBLEMedium () <GNCCentralManagerDelegate, CBCentralManagerDelegate>
|
||||
@end
|
||||
|
||||
@implementation GNCBLEMedium {
|
||||
dispatch_queue_t _queue;
|
||||
id<GNCCentralManager> _centralManager;
|
||||
|
||||
// The active GATT server, or @nil if one hasn't been started yet.
|
||||
GNCBLEGATTServer *_server;
|
||||
|
||||
// The service that is being actively scanned for, or @c nil if not currently scanning.
|
||||
CBUUID *_serviceUUID;
|
||||
|
||||
// The handler called when an advertisement for the service represented by @c _serviceUUID has
|
||||
// been discovered. This will be called continuously, until the peripheral disappears.
|
||||
GNCAdvertisementFoundHandler _advertisementFoundHandler;
|
||||
|
||||
// A peripheral to connection completion handler map. Used to track connection attempts. When a
|
||||
// connection attempt has succeeded or failed, the completion handler is called and removed from
|
||||
// the map.
|
||||
NSMutableDictionary<NSUUID *, GNCGATTConnectionCompletionHandler> *_connectionCompletionHandlers;
|
||||
|
||||
// A peripheral to disconnection handler map. Used to track when a peripheral becomes
|
||||
// disconnected. Once disconnected, the completion handler is called and removed from the map.
|
||||
NSMutableDictionary<NSUUID *, GNCGATTDisconnectionHandler> *_disconnectionHandlers;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
CBCentralManager *centralManager =
|
||||
[[CBCentralManager alloc] initWithDelegate:self
|
||||
queue:_queue
|
||||
options:@{CBCentralManagerOptionShowPowerAlertKey : @NO}];
|
||||
dispatch_queue_t queue = dispatch_queue_create(kBLEMediumQueueLabel, DISPATCH_QUEUE_SERIAL);
|
||||
return [self initWithCentralManager:centralManager queue:queue];
|
||||
}
|
||||
|
||||
// This is private and should only be used for tests. The provided central manager must call
|
||||
// delegate methods on the main queue.
|
||||
- (instancetype)initWithCentralManager:(id<GNCCentralManager>)centralManager
|
||||
queue:(nullable dispatch_queue_t)queue {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_queue = queue ?: dispatch_get_main_queue();
|
||||
_centralManager = centralManager;
|
||||
_centralManager.centralDelegate = self;
|
||||
_connectionCompletionHandlers = [NSMutableDictionary dictionary];
|
||||
_disconnectionHandlers = [NSMutableDictionary dictionary];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)supportsExtendedAdvertisements {
|
||||
// TODO(b/294736083): CoreBluetooth doesn't support actually advertising any extensions, however
|
||||
// some devices can scan for them if the feature is available. If we return @c YES from this
|
||||
// method, we would be enabling advertising extensions (which won't work), so we must return @c NO
|
||||
// until we add support for a new method to check only if extended scans are supported.
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)startAdvertisingData:(NSDictionary<CBUUID *, NSData *> *)serviceData
|
||||
completionHandler:(nullable GNCStartAdvertisingCompletionHandler)completionHandler {
|
||||
dispatch_async(_queue, ^{
|
||||
if (!_server) {
|
||||
_server = [[GNCBLEGATTServer alloc] init];
|
||||
}
|
||||
[_server startAdvertisingData:serviceData completionHandler:completionHandler];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)startScanningForService:(CBUUID *)serviceUUID
|
||||
advertisementFoundHandler:(GNCAdvertisementFoundHandler)advertisementFoundHandler
|
||||
completionHandler:(nullable GNCStartScanningCompletionHandler)completionHandler {
|
||||
dispatch_async(_queue, ^{
|
||||
if (_serviceUUID) {
|
||||
if (completionHandler) {
|
||||
completionHandler(AlreadyScanningError());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_serviceUUID = serviceUUID;
|
||||
_advertisementFoundHandler = advertisementFoundHandler;
|
||||
|
||||
[self internalStartScanningIfPoweredOn];
|
||||
if (completionHandler) {
|
||||
completionHandler(nil);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)startGATTServerWithCompletionHandler:
|
||||
(nullable GNCGATTServerCompletionHandler)completionHandler {
|
||||
dispatch_async(_queue, ^{
|
||||
if (!_server) {
|
||||
_server = [[GNCBLEGATTServer alloc] init];
|
||||
}
|
||||
if (completionHandler) {
|
||||
completionHandler(_server, nil);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)connectToGATTServerForPeripheral:(id<GNCPeripheral>)remotePeripheral
|
||||
disconnectionHandler:(nullable GNCGATTDisconnectionHandler)disconnectionHandler
|
||||
completionHandler:
|
||||
(nullable GNCGATTConnectionCompletionHandler)completionHandler {
|
||||
dispatch_async(_queue, ^{
|
||||
_disconnectionHandlers[remotePeripheral.identifier] = disconnectionHandler;
|
||||
_connectionCompletionHandlers[remotePeripheral.identifier] = completionHandler;
|
||||
[_centralManager connectPeripheral:remotePeripheral options:@{}];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Internal
|
||||
|
||||
- (void)internalStartScanningIfPoweredOn {
|
||||
dispatch_assert_queue(_queue);
|
||||
// Scanning can only be done when powered on and must be restarted if bluetooth is turned off
|
||||
// then back on. This will be called anytime the central manager's state changes, so
|
||||
// @c scanForPeripheralsWithServices:options: will be called anytime state transitions back to
|
||||
// powered on.
|
||||
if (_centralManager.state == CBManagerStatePoweredOn && _serviceUUID != nil) {
|
||||
// Stop scanning just in case something outside of this class is already scanning.
|
||||
[_centralManager stopScan];
|
||||
[_centralManager
|
||||
scanForPeripheralsWithServices:@[ _serviceUUID ]
|
||||
// Nearby relies on the existence of an advertisement for endpoint
|
||||
// discovery/lost events, so we must set this key to keep the stream
|
||||
// of duplicate delegate events flowing. This has adverse effect on
|
||||
// battery life, but currently necessary.
|
||||
options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES}];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSDictionary<CBUUID *, NSData *> *)decodeAdvertisementData:
|
||||
(NSDictionary<NSString *, id> *)advertisementData {
|
||||
dispatch_assert_queue(_queue);
|
||||
// If service data is available, return it directly.
|
||||
NSDictionary<CBUUID *, NSData *> *serviceData =
|
||||
advertisementData[CBAdvertisementDataServiceDataKey];
|
||||
if (serviceData) {
|
||||
return serviceData;
|
||||
}
|
||||
|
||||
// Apple devices don't support advertising service data, so Apple devices advertise a base64
|
||||
// encoded local name, while other devices advertise service data. Here we attempt to reconstruct
|
||||
// service data by decoding the local name. If successful, this is possibly a Nearby advertisement
|
||||
// on an Apple device.
|
||||
NSString *localName = advertisementData[CBAdvertisementDataLocalNameKey];
|
||||
if (!localName) {
|
||||
return @{};
|
||||
}
|
||||
NSData *data = [[NSData alloc] initWithWebSafeBase64EncodedString:localName];
|
||||
|
||||
// A Nearby Apple advertisement should only have a single service, so simply grab the first one if
|
||||
// it exists.
|
||||
NSArray<CBUUID *> *serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey];
|
||||
CBUUID *serviceUUID = serviceUUIDs.firstObject;
|
||||
if (data && serviceUUID) {
|
||||
return @{serviceUUID : data};
|
||||
}
|
||||
|
||||
return @{};
|
||||
}
|
||||
|
||||
#pragma mark - GNCCentralManagerDelegate
|
||||
|
||||
- (void)gnc_centralManagerDidUpdateState:(id<GNCCentralManager>)central {
|
||||
dispatch_assert_queue(_queue);
|
||||
[self internalStartScanningIfPoweredOn];
|
||||
}
|
||||
|
||||
- (void)gnc_centralManager:(id<GNCCentralManager>)central
|
||||
didDiscoverPeripheral:(id<GNCPeripheral>)peripheral
|
||||
advertisementData:(NSDictionary<NSString *, id> *)advertisementData
|
||||
RSSI:(NSNumber *)RSSI {
|
||||
dispatch_assert_queue(_queue);
|
||||
if (_advertisementFoundHandler) {
|
||||
_advertisementFoundHandler(peripheral, [self decodeAdvertisementData:advertisementData]);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)gnc_centralManager:(id<GNCCentralManager>)central
|
||||
didConnectPeripheral:(id<GNCPeripheral>)peripheral {
|
||||
dispatch_assert_queue(_queue);
|
||||
GNCGATTConnectionCompletionHandler handler = _connectionCompletionHandlers[peripheral.identifier];
|
||||
_connectionCompletionHandlers[peripheral.identifier] = nil;
|
||||
if (handler) {
|
||||
GNCBLEGATTClient *client = [[GNCBLEGATTClient alloc] initWithPeripheral:peripheral];
|
||||
handler(client, nil);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)gnc_centralManager:(id<GNCCentralManager>)central
|
||||
didFailToConnectPeripheral:(id<GNCPeripheral>)peripheral
|
||||
error:(nullable NSError *)error {
|
||||
dispatch_assert_queue(_queue);
|
||||
GNCGATTConnectionCompletionHandler handler = _connectionCompletionHandlers[peripheral.identifier];
|
||||
_connectionCompletionHandlers[peripheral.identifier] = nil;
|
||||
if (handler) {
|
||||
handler(nil, error);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)gnc_centralManager:(id<GNCCentralManager>)central
|
||||
didDisconnectPeripheral:(id<GNCPeripheral>)peripheral
|
||||
error:(nullable NSError *)error {
|
||||
dispatch_assert_queue(_queue);
|
||||
GNCGATTDisconnectionHandler handler = _disconnectionHandlers[peripheral.identifier];
|
||||
_disconnectionHandlers[peripheral.identifier] = nil;
|
||||
if (handler) {
|
||||
handler();
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - CBCentralManagerDelegate
|
||||
|
||||
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
|
||||
dispatch_async(_queue, ^{
|
||||
[self gnc_centralManagerDidUpdateState:central];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)centralManager:(CBCentralManager *)central
|
||||
didDiscoverPeripheral:(CBPeripheral *)peripheral
|
||||
advertisementData:(NSDictionary<NSString *, id> *)advertisementData
|
||||
RSSI:(NSNumber *)RSSI {
|
||||
dispatch_async(_queue, ^{
|
||||
[self gnc_centralManager:central
|
||||
didDiscoverPeripheral:peripheral
|
||||
advertisementData:advertisementData
|
||||
RSSI:RSSI];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
|
||||
dispatch_async(_queue, ^{
|
||||
[self gnc_centralManager:central didConnectPeripheral:peripheral];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)centralManager:(CBCentralManager *)central
|
||||
didFailToConnectPeripheral:(CBPeripheral *)peripheral
|
||||
error:(nullable NSError *)error {
|
||||
dispatch_async(_queue, ^{
|
||||
[self gnc_centralManager:central didFailToConnectPeripheral:peripheral error:error];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)centralManager:(CBCentralManager *)central
|
||||
didDisconnectPeripheral:(CBPeripheral *)peripheral
|
||||
error:(nullable NSError *)error {
|
||||
dispatch_async(_queue, ^{
|
||||
[self gnc_centralManager:central didDisconnectPeripheral:peripheral error:error];
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,181 @@
|
||||
// Copyright 2023 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>
|
||||
|
||||
@protocol GNCCentralManagerDelegate;
|
||||
@protocol GNCPeripheral;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/** Protocol which helps create a fake of a @c CBCentralManager to inject for testing. */
|
||||
@protocol GNCCentralManager
|
||||
|
||||
/** Shadow property of a @c CBCentralManagerDelegate. */
|
||||
@property(weak, nonatomic, nullable) id<GNCCentralManagerDelegate> centralDelegate;
|
||||
|
||||
/**
|
||||
* The current state of the manager.
|
||||
*
|
||||
* This state is initially set to @c CBManagerStateUnknown. When the state updates, the manager
|
||||
* calls its delegate’s @c gnc_centralManagerDidUpdateState: method.
|
||||
*/
|
||||
@property(nonatomic, assign, readonly) CBManagerState state;
|
||||
|
||||
/**
|
||||
* Scans for peripherals that are advertising services.
|
||||
*
|
||||
* You can provide an array of @c CBUUID objects, representing service UUIDs, in the @c serviceUUIDs
|
||||
* parameter. When you do, the central manager returns only peripherals that advertise the services
|
||||
* you specify. If the @c serviceUUIDs parameter is @c nil, this method returns all discovered
|
||||
* peripherals, regardless of their supported services.
|
||||
*
|
||||
* @note The recommended practice is to populate the @c serviceUUIDs parameter rather than leaving
|
||||
* it @c nil.
|
||||
*
|
||||
* If the central manager is actively scanning with one set of parameters and it receives another
|
||||
* set to scan, the new parameters override the previous set. When the central manager discovers a
|
||||
* peripheral, it calls the @c gnc_centralManager:didDiscoverPeripheral:advertisementData:RSSI:
|
||||
* method of its delegate object.
|
||||
*
|
||||
* Your app can scan for Bluetooth devices in the background by specifying the @c bluetooth-central
|
||||
* background mode. To do this, your app must explicitly scan for one or more services by specifying
|
||||
* them in the @c serviceUUIDs parameter. The CBCentralManager scan option has no effect while
|
||||
* scanning in the background.
|
||||
*
|
||||
* @param serviceUUIDs An array of @c CBUUID objects that the app is interested in. Each @c CBUUID
|
||||
* object represents the UUID of a service that a peripheral advertises.
|
||||
* @param options A dictionary of options for customizing the scan.
|
||||
*/
|
||||
- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs
|
||||
options:(nullable NSDictionary<NSString *, id> *)options;
|
||||
|
||||
/**
|
||||
* Establishes a local connection to a peripheral.
|
||||
*
|
||||
* After successfully establishing a local connection to a peripheral, the central manager object
|
||||
* calls the @c gnc_centralManager:didConnectPeripheral: method of its delegate object. If the
|
||||
* connection attempt fails, the central manager object calls the
|
||||
* @c gnc_centralManager:didFailToConnectPeripheral:error: method of its delegate object instead.
|
||||
* Attempts to connect to a peripheral don’t time out. To explicitly cancel a pending connection to
|
||||
* a peripheral, call the @c cancelPeripheralConnection: method. Deallocating @c peripheral also
|
||||
* implicitly calls @c cancelPeripheralConnection:.
|
||||
*
|
||||
* @param peripheral The peripheral to which the central is attempting to connect.
|
||||
* @param options A dictionary to customize the behavior of the connection.
|
||||
*/
|
||||
- (void)connectPeripheral:(id<GNCPeripheral>)peripheral
|
||||
options:(nullable NSDictionary<NSString *, id> *)options;
|
||||
|
||||
/** Asks the central manager to stop scanning for peripherals. */
|
||||
- (void)stopScan;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* Protocol which helps the @c GNCCentralManager wrap a @c CBCentralManagerDelegate for
|
||||
* testing.
|
||||
*/
|
||||
@protocol GNCCentralManagerDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* Tells the delegate the central manager’s state updated.
|
||||
*
|
||||
* You implement this required method to ensure that the central device supports Bluetooth low
|
||||
* energy and that it’s available to use. You should issue commands to the central manager only when
|
||||
* the central manager’s @c state indicates it’s powered on. A state with a value lower than
|
||||
* @c CBManagerStatePoweredOn implies that scanning has stopped, which in turn disconnects any
|
||||
* previously-connected peripherals. If the state moves below @c CBManagerStatePoweredOff, all
|
||||
* @c CBPeripheral objects obtained from this central manager become invalid; you must retrieve or
|
||||
* discover these peripherals again.
|
||||
*
|
||||
* @param central The central manager whose state has changed.
|
||||
*/
|
||||
- (void)gnc_centralManagerDidUpdateState:(id<GNCCentralManager>)central;
|
||||
|
||||
/**
|
||||
* Tells the delegate the central manager discovered a peripheral while scanning for devices.
|
||||
*
|
||||
* You must retain a local copy of the peripheral if you want to perform commands on it. Use the
|
||||
* RSSI data to determine the proximity of a discoverable peripheral device, and whether you want to
|
||||
* connect to it automatically.
|
||||
*
|
||||
* @param central The central manager that provides the update.
|
||||
* @param peripheral The discovered peripheral.
|
||||
* @param advertisementData A dictionary containing any advertisement data.
|
||||
* @param RSSI The current received signal strength indicator (RSSI) of the peripheral, in decibels.
|
||||
*/
|
||||
- (void)gnc_centralManager:(id<GNCCentralManager>)central
|
||||
didDiscoverPeripheral:(id<GNCPeripheral>)peripheral
|
||||
advertisementData:(NSDictionary<NSString *, id> *)advertisementData
|
||||
RSSI:(NSNumber *)RSSI;
|
||||
|
||||
/**
|
||||
* Tells the delegate that the central manager connected to a peripheral.
|
||||
*
|
||||
* The manager invokes this method when a call to @c connectPeripheral:options: succeeds. You
|
||||
* typically implement this method to set the peripheral’s delegate and discover its services.
|
||||
*
|
||||
* @param central The central manager that provides this information.
|
||||
* @param peripheral The now-connected peripheral.
|
||||
*/
|
||||
- (void)gnc_centralManager:(id<GNCCentralManager>)central
|
||||
didConnectPeripheral:(id<GNCPeripheral>)peripheral;
|
||||
|
||||
/**
|
||||
* Tells the delegate the central manager failed to create a connection with a peripheral.
|
||||
*
|
||||
* The manager invokes this method when a connection initiated with the
|
||||
* @c connectPeripheral:options: method fails to complete. Because connection attempts don’t time
|
||||
* out, a failed connection usually indicates a transient issue, in which case you may attempt
|
||||
* connecting to the peripheral again.
|
||||
*
|
||||
* @param central The central manager that provides this information.
|
||||
* @param peripheral The peripheral that failed to connect.
|
||||
* @param error The cause of the failure, or @c nil if no error occurred.
|
||||
*/
|
||||
- (void)gnc_centralManager:(id<GNCCentralManager>)central
|
||||
didFailToConnectPeripheral:(id<GNCPeripheral>)peripheral
|
||||
error:(nullable NSError *)error;
|
||||
|
||||
/**
|
||||
* Tells the delegate that the central manager disconnected from a peripheral.
|
||||
*
|
||||
* The manager invokes this method when disconnecting a peripheral previously connected with the
|
||||
* @c connectPeripheral:options: method. The error parameter contains the reason for the
|
||||
* disconnection, unless the disconnect resulted from a call to @c cancelPeripheralConnection:.
|
||||
*
|
||||
* All services, characteristics, and characteristic descriptors of a peripheral become invalidated
|
||||
* after it disconnects.
|
||||
*
|
||||
* @param central The central manager that provides this information.
|
||||
* @param peripheral The now-disconnected peripheral.
|
||||
* @param error The cause of the failure, or @c nil if no error occurred.
|
||||
*/
|
||||
- (void)gnc_centralManager:(id<GNCCentralManager>)central
|
||||
didDisconnectPeripheral:(id<GNCPeripheral>)peripheral
|
||||
error:(nullable NSError *)error;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* Declares that @c CBCentralManager implements the @c GNCCentralManager protocol.
|
||||
*
|
||||
* This allows us to directly use a @c CBCentralManager as a @c GNCCentralManager.
|
||||
*/
|
||||
@interface CBCentralManager () <GNCCentralManager>
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2023 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/GNCCentralManager.h"
|
||||
|
||||
#import <CoreBluetooth/CoreBluetooth.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation CBCentralManager (GNCCentralManagerAdditions)
|
||||
|
||||
- (void)setCentralDelegate:(nullable id<GNCCentralManagerDelegate>)centralDelegate {
|
||||
NSAssert([centralDelegate conformsToProtocol:@protocol(CBCentralManagerDelegate)],
|
||||
@"centralDelegate must conform to protocol CBCentralManagerDelegate");
|
||||
self.delegate = (id<CBCentralManagerDelegate>)centralDelegate;
|
||||
}
|
||||
|
||||
- (nullable id<GNCCentralManagerDelegate>)centralDelegate {
|
||||
return (id<GNCCentralManagerDelegate>)self.delegate;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -43,6 +43,16 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
*/
|
||||
@property(retain, readonly, nullable) NSArray<CBService *> *services;
|
||||
|
||||
/**
|
||||
* The UUID associated with the peer.
|
||||
*
|
||||
* The value of this property represents the unique identifier of the peer. The first time a local
|
||||
* manager encounters a peer, the system assigns the peer a UUID, represented by a new @c NSUUID
|
||||
* object. Peers use @c NSUUID instances to identify themselves, instead of by the @c CBUUID objects
|
||||
* that identify a peripheral’s services, characteristics, and descriptors.
|
||||
*/
|
||||
@property(readonly, nonatomic) NSUUID *identifier;
|
||||
|
||||
/**
|
||||
* Discovers the specified services of the peripheral.
|
||||
*
|
||||
|
||||
@@ -19,7 +19,16 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@interface NSData (GNCWebSafeBase64)
|
||||
|
||||
/** Creates a Base64 encoded string from the data using websafe characters and no padding. */
|
||||
- (NSString *)webSafebase64EncodedString;
|
||||
- (NSString *)webSafeBase64EncodedString;
|
||||
|
||||
/**
|
||||
* Initializes a data object with the given Base64 encoded string.
|
||||
*
|
||||
* @param base64String A Base64 encoded string.
|
||||
* @return A data object built by Base64 decoding the provided string. Returns @c nil if the data
|
||||
* object could not be decoded.
|
||||
*/
|
||||
- (nullable instancetype)initWithWebSafeBase64EncodedString:(NSString *)base64String;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -14,11 +14,13 @@
|
||||
|
||||
#import "internal/platform/implementation/apple/Mediums/BLEv2/NSData+GNCWebSafeBase64.h"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation NSData (GNCWebSafeBase64)
|
||||
|
||||
- (NSString *)webSafebase64EncodedString {
|
||||
- (NSString *)webSafeBase64EncodedString {
|
||||
NSString *encoded = [self base64EncodedStringWithOptions:0];
|
||||
|
||||
// Convert the standard base64 characters to URL safe variants.
|
||||
@@ -29,6 +31,20 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return encoded;
|
||||
}
|
||||
|
||||
- (nullable instancetype)initWithWebSafeBase64EncodedString:(NSString *)base64String {
|
||||
// Convert the URL safe base64 characters to the standard variants.
|
||||
base64String = [base64String stringByReplacingOccurrencesOfString:@"-" withString:@"+"];
|
||||
base64String = [base64String stringByReplacingOccurrencesOfString:@"_" withString:@"/"];
|
||||
|
||||
// @c initWithBase64EncodedString:options: requires a padded base64 string. Append enough "="
|
||||
// characters to make the string a multiple of 4.
|
||||
NSUInteger paddedLength = base64String.length + ((4 - (base64String.length % 4)) % 4);
|
||||
base64String = [base64String stringByPaddingToLength:paddedLength
|
||||
withString:@"="
|
||||
startingAtIndex:0];
|
||||
return [self initWithBase64EncodedString:base64String options:0];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -22,6 +22,8 @@ objc_library(
|
||||
"BLEv2/GNCBLEGATTCharacteristic.m",
|
||||
"BLEv2/GNCBLEGATTClient.m",
|
||||
"BLEv2/GNCBLEGATTServer.m",
|
||||
"BLEv2/GNCBLEMedium.m",
|
||||
"BLEv2/GNCCentralManager.m",
|
||||
"BLEv2/GNCPeripheral.m",
|
||||
"BLEv2/GNCPeripheralManager.m",
|
||||
"BLEv2/NSData+GNCWebSafeBase64.m",
|
||||
@@ -44,6 +46,8 @@ objc_library(
|
||||
"BLEv2/GNCBLEGATTCharacteristic.h",
|
||||
"BLEv2/GNCBLEGATTClient.h",
|
||||
"BLEv2/GNCBLEGATTServer.h",
|
||||
"BLEv2/GNCBLEMedium.h",
|
||||
"BLEv2/GNCCentralManager.h",
|
||||
"BLEv2/GNCPeripheral.h",
|
||||
"BLEv2/GNCPeripheralManager.h",
|
||||
"BLEv2/NSData+GNCWebSafeBase64.h",
|
||||
|
||||
@@ -24,13 +24,18 @@ objc_library(
|
||||
testonly = True,
|
||||
srcs = [
|
||||
"GNCBLEGATTCharacteristicTest.mm",
|
||||
"GNCBLEGATTClient+Testing.h",
|
||||
"GNCBLEGATTClientTest.m",
|
||||
"GNCBLEGATTServer+Testing.h",
|
||||
"GNCBLEGATTServerTest.m",
|
||||
"GNCBLEMedium+Testing.h",
|
||||
"GNCBLEMediumTest.m",
|
||||
"GNCBLEUtilsTest.mm",
|
||||
"GNCBleTest.mm",
|
||||
"GNCBluetoothAdapterTest.mm",
|
||||
"GNCCryptoTest.mm",
|
||||
"GNCFakeCentralManager.h",
|
||||
"GNCFakeCentralManager.m",
|
||||
"GNCFakePeripheral.h",
|
||||
"GNCFakePeripheral.m",
|
||||
"GNCFakePeripheralManager.h",
|
||||
@@ -43,7 +48,6 @@ objc_library(
|
||||
"NSData+GNCWebSafeBase64Test.m",
|
||||
],
|
||||
deps = [
|
||||
":GNCBLEGATTClient_Testing",
|
||||
"//internal/platform:base",
|
||||
"//internal/platform/implementation:comm",
|
||||
"//internal/platform/implementation:platform",
|
||||
@@ -58,15 +62,6 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "GNCBLEGATTClient_Testing",
|
||||
hdrs = ["GNCBLEGATTClient+Testing.h"],
|
||||
deps = [
|
||||
"//internal/platform/implementation/apple/Mediums",
|
||||
"//third_party/apple_frameworks:Foundation",
|
||||
],
|
||||
)
|
||||
|
||||
ios_unit_test(
|
||||
name = "PlatformTests",
|
||||
minimum_os_version = IOS_MINIMUM_OS,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2023 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/GNCBLEMedium.h"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@protocol GNCCentralManager;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface GNCBLEMedium (Testing)
|
||||
|
||||
/**
|
||||
* Creates a BLE Medium with a provided central manager.
|
||||
*
|
||||
* This is only exposed for testing and can be used to inject a fake central manager.
|
||||
*
|
||||
* @param centralManager The central manager instance.
|
||||
* @param queue The queue to run on, this must match the queue that the central manager's delegate
|
||||
* is running on. Defaults to the main queue when @c nil.
|
||||
*/
|
||||
- (instancetype)initWithCentralManager:(id<GNCCentralManager>)centralManager
|
||||
queue:(nullable dispatch_queue_t)queue;
|
||||
|
||||
- (NSDictionary<CBUUID *, NSData *> *)decodeAdvertisementData:
|
||||
(NSDictionary<NSString *, id> *)advertisementData;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,294 @@
|
||||
// Copyright 2023 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/GNCBLEMedium.h"
|
||||
|
||||
#import <CoreBluetooth/CoreBluetooth.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h"
|
||||
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h"
|
||||
#import "internal/platform/implementation/apple/Tests/GNCBLEMedium+Testing.h"
|
||||
#import "internal/platform/implementation/apple/Tests/GNCFakeCentralManager.h"
|
||||
#import "internal/platform/implementation/apple/Tests/GNCFakePeripheral.h"
|
||||
|
||||
static NSString *const kServiceUUID = @"0000FEF3-0000-1000-8000-00805F9B34FB";
|
||||
|
||||
@interface GNCBLEMediumTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation GNCBLEMediumTest
|
||||
|
||||
#pragma mark - Supports Extended Advertisements
|
||||
|
||||
- (void)testSupportsExtendedAdvertisements {
|
||||
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
|
||||
|
||||
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
|
||||
|
||||
XCTAssertFalse([medium supportsExtendedAdvertisements]);
|
||||
}
|
||||
|
||||
#pragma mark - Start Scanning
|
||||
|
||||
- (void)testStartScanning {
|
||||
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
|
||||
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
|
||||
XCTestExpectation *startScanningExpectation =
|
||||
[[XCTestExpectation alloc] initWithDescription:@"Start scanning."];
|
||||
XCTestExpectation *advertisementFoundExpectation =
|
||||
[[XCTestExpectation alloc] initWithDescription:@"Advertisement found."];
|
||||
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID];
|
||||
|
||||
[fakeCentralManager simulateCentralManagerDidUpdateState:CBManagerStatePoweredOn];
|
||||
|
||||
[medium startScanningForService:serviceUUID
|
||||
advertisementFoundHandler:^(id<GNCPeripheral> peripheral,
|
||||
NSDictionary<CBUUID *, NSData *> *data) {
|
||||
NSDictionary<CBUUID *, NSData *> *expected = @{
|
||||
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
|
||||
};
|
||||
XCTAssertEqualObjects(expected, data);
|
||||
[advertisementFoundExpectation fulfill];
|
||||
}
|
||||
completionHandler:^(NSError *error) {
|
||||
XCTAssertNil(error);
|
||||
[startScanningExpectation fulfill];
|
||||
}];
|
||||
|
||||
[self waitForExpectations:@[ startScanningExpectation ] timeout:3];
|
||||
|
||||
XCTAssertEqualObjects(@[ serviceUUID ], fakeCentralManager.serviceUUIDs);
|
||||
|
||||
[fakeCentralManager
|
||||
simulateCentralManagerDidDiscoverPeripheral:[[GNCFakePeripheral alloc] init]
|
||||
advertisementData:@{
|
||||
CBAdvertisementDataLocalNameKey : @"dGVzdA",
|
||||
CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ],
|
||||
}];
|
||||
|
||||
[self waitForExpectations:@[ advertisementFoundExpectation ] timeout:3];
|
||||
}
|
||||
|
||||
- (void)testAlreadyScanning {
|
||||
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
|
||||
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
|
||||
XCTestExpectation *expectation =
|
||||
[[XCTestExpectation alloc] initWithDescription:@"Start scanning."];
|
||||
|
||||
[medium startScanningForService:[CBUUID UUIDWithString:kServiceUUID]
|
||||
advertisementFoundHandler:^(id<GNCPeripheral> peripheral,
|
||||
NSDictionary<CBUUID *, NSData *> *data) {
|
||||
}
|
||||
completionHandler:nil];
|
||||
|
||||
[medium startScanningForService:[CBUUID UUIDWithString:kServiceUUID]
|
||||
advertisementFoundHandler:^(id<GNCPeripheral> peripheral,
|
||||
NSDictionary<CBUUID *, NSData *> *data) {
|
||||
}
|
||||
completionHandler:^(NSError *error) {
|
||||
XCTAssertNotNil(error);
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:3];
|
||||
}
|
||||
|
||||
#pragma mark - Decode Advertisement Data
|
||||
|
||||
- (void)testDecodeAndroidStyleAdvertisementData {
|
||||
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
|
||||
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
|
||||
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID];
|
||||
|
||||
NSDictionary<CBUUID *, NSData *> *expected = @{
|
||||
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
|
||||
};
|
||||
|
||||
NSDictionary<NSString *, id> *data = @{
|
||||
CBAdvertisementDataServiceDataKey : @{
|
||||
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
|
||||
},
|
||||
};
|
||||
NSDictionary<CBUUID *, NSData *> *actual = [medium decodeAdvertisementData:data];
|
||||
|
||||
XCTAssertEqualObjects(expected, actual);
|
||||
}
|
||||
|
||||
- (void)testDecodeAndroidStyleAdvertisementDataWithLocalName {
|
||||
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
|
||||
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
|
||||
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID];
|
||||
|
||||
NSDictionary<CBUUID *, NSData *> *expected = @{
|
||||
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
|
||||
};
|
||||
|
||||
NSDictionary<NSString *, id> *data = @{
|
||||
CBAdvertisementDataLocalNameKey : @"Nearby", // Just happens to be base64 decodable.
|
||||
CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ],
|
||||
CBAdvertisementDataServiceDataKey : @{
|
||||
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
|
||||
},
|
||||
};
|
||||
NSDictionary<CBUUID *, NSData *> *actual = [medium decodeAdvertisementData:data];
|
||||
|
||||
XCTAssertEqualObjects(expected, actual);
|
||||
}
|
||||
|
||||
- (void)testDecodeAppleStyleAdvertisementData {
|
||||
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
|
||||
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
|
||||
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID];
|
||||
|
||||
NSDictionary<CBUUID *, NSData *> *expected = @{
|
||||
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
|
||||
};
|
||||
|
||||
NSDictionary<NSString *, id> *data = @{
|
||||
CBAdvertisementDataLocalNameKey : @"dGVzdA",
|
||||
CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ],
|
||||
};
|
||||
NSDictionary<CBUUID *, NSData *> *actual = [medium decodeAdvertisementData:data];
|
||||
|
||||
XCTAssertEqualObjects(expected, actual);
|
||||
}
|
||||
|
||||
- (void)testDecodeInvalidAdvertisementData {
|
||||
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
|
||||
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
|
||||
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID];
|
||||
|
||||
NSDictionary<NSString *, id> *data = @{
|
||||
CBAdvertisementDataLocalNameKey : @"!@#$",
|
||||
CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ],
|
||||
};
|
||||
NSDictionary<CBUUID *, NSData *> *actual = [medium decodeAdvertisementData:data];
|
||||
|
||||
XCTAssertEqualObjects(@{}, actual);
|
||||
}
|
||||
|
||||
- (void)testDecodeEmptyAdvertisementData {
|
||||
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
|
||||
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
|
||||
|
||||
NSDictionary<CBUUID *, NSData *> *actual = [medium decodeAdvertisementData:@{}];
|
||||
|
||||
XCTAssertEqualObjects(@{}, actual);
|
||||
}
|
||||
|
||||
#pragma mark - Start GATT Server
|
||||
|
||||
- (void)testStartGATTServer {
|
||||
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
|
||||
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
|
||||
XCTestExpectation *expectation =
|
||||
[[XCTestExpectation alloc] initWithDescription:@"Start GATT server."];
|
||||
|
||||
[medium startGATTServerWithCompletionHandler:^(GNCBLEGATTServer *server, NSError *error) {
|
||||
XCTAssertNotNil(server);
|
||||
XCTAssertNil(error);
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:3];
|
||||
}
|
||||
|
||||
#pragma mark - Start Advertising
|
||||
|
||||
- (void)testStartAdvertising {
|
||||
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
|
||||
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
|
||||
XCTestExpectation *expectation =
|
||||
[[XCTestExpectation alloc] initWithDescription:@"Start advertising."];
|
||||
|
||||
// Start advertising is fully covered with @c GNCBLEGATTServer tests. We are passing invalid
|
||||
// advertising data here so we can test code paths relevant to @c GNCBLEMedium, but bail early
|
||||
// enough to avoid making actual CoreBluetooth calls.
|
||||
[medium startAdvertisingData:@{}
|
||||
completionHandler:^(NSError *error) {
|
||||
XCTAssertNotNil(error);
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:3];
|
||||
}
|
||||
|
||||
#pragma mark - Connect
|
||||
|
||||
- (void)testSuccessfulConnect {
|
||||
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
|
||||
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
|
||||
XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Connect."];
|
||||
|
||||
[medium connectToGATTServerForPeripheral:[[GNCFakePeripheral alloc] init]
|
||||
disconnectionHandler:nil
|
||||
completionHandler:^(GNCBLEGATTClient *client, NSError *error) {
|
||||
XCTAssertNotNil(client);
|
||||
XCTAssertNil(error);
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:3];
|
||||
}
|
||||
|
||||
- (void)testFailedConnect {
|
||||
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
|
||||
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
|
||||
XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Connect."];
|
||||
|
||||
fakeCentralManager.didFailToConnectPeripheralError = [NSError errorWithDomain:@"fake"
|
||||
code:0
|
||||
userInfo:nil];
|
||||
|
||||
[medium connectToGATTServerForPeripheral:[[GNCFakePeripheral alloc] init]
|
||||
disconnectionHandler:nil
|
||||
completionHandler:^(GNCBLEGATTClient *client, NSError *error) {
|
||||
XCTAssertNil(client);
|
||||
XCTAssertNotNil(error);
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:3];
|
||||
}
|
||||
|
||||
- (void)testDisconnect {
|
||||
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
|
||||
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
|
||||
XCTestExpectation *connectExpectation =
|
||||
[[XCTestExpectation alloc] initWithDescription:@"Connect."];
|
||||
XCTestExpectation *disconnectExpectation =
|
||||
[[XCTestExpectation alloc] initWithDescription:@"Disconnect."];
|
||||
|
||||
GNCFakePeripheral *peripheral = [[GNCFakePeripheral alloc] init];
|
||||
|
||||
[medium connectToGATTServerForPeripheral:peripheral
|
||||
disconnectionHandler:^() {
|
||||
[disconnectExpectation fulfill];
|
||||
}
|
||||
completionHandler:^(GNCBLEGATTClient *client, NSError *error) {
|
||||
XCTAssertNotNil(client);
|
||||
XCTAssertNil(error);
|
||||
[connectExpectation fulfill];
|
||||
}];
|
||||
|
||||
[self waitForExpectations:@[ connectExpectation ] timeout:3];
|
||||
|
||||
[fakeCentralManager simulateCentralManagerDidDisconnectPeripheral:peripheral];
|
||||
|
||||
[self waitForExpectations:@[ disconnectExpectation ] timeout:3];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2023 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>
|
||||
|
||||
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCCentralManager.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/** A fake implementation of @c GNCCentralManager to inject for testing. */
|
||||
@interface GNCFakeCentralManager : NSObject <GNCCentralManager>
|
||||
|
||||
/** The list of services being scanned for. */
|
||||
@property(nonatomic, nullable, readonly) NSArray<CBUUID *> *serviceUUIDs;
|
||||
|
||||
/**
|
||||
* Similates a @c connectPeripheral:options: error.
|
||||
*
|
||||
* Setting this error to a value other than @c nil will simulate a failure when calling
|
||||
* @c connectPeripheral:options: and will call the
|
||||
* @c gnc_centralManager:didFailToConnectPeripheral:error: delegate method with the provided error.
|
||||
*/
|
||||
@property(nonatomic, nullable, readwrite) NSError *didFailToConnectPeripheralError;
|
||||
|
||||
/**
|
||||
* Simulates a state update event.
|
||||
*
|
||||
* Updates the central manager state to the provided value and calls the
|
||||
* @c gnc_centralManagerDidUpdateState: delegate method.
|
||||
*
|
||||
* @param fakeState The new state to transition to.
|
||||
*/
|
||||
- (void)simulateCentralManagerDidUpdateState:(CBManagerState)fakeState;
|
||||
|
||||
/**
|
||||
* Simulates a peripheral discovery event.
|
||||
*
|
||||
* Calls the @c gnc_centralManager:didDiscoverPeripheral:advertisementData:RSSI: delegate method.
|
||||
*
|
||||
* @param peripheral The discovered peripheral.
|
||||
* @param peripheral A dictionary containing any advertisement data.
|
||||
*/
|
||||
- (void)simulateCentralManagerDidDiscoverPeripheral:(id<GNCPeripheral>)peripheral
|
||||
advertisementData:
|
||||
(NSDictionary<NSString *, id> *)advertisementData;
|
||||
|
||||
/**
|
||||
* Simulates a peripheral disconnection event.
|
||||
*
|
||||
* Calls the @c gnc_centralManager:didDisconnectPeripheral:error: delegate method.
|
||||
*
|
||||
* @param peripheral The now-disconnected peripheral.
|
||||
*/
|
||||
- (void)simulateCentralManagerDidDisconnectPeripheral:(id<GNCPeripheral>)peripheral;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright 2023 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/Tests/GNCFakeCentralManager.h"
|
||||
|
||||
#import <CoreBluetooth/CoreBluetooth.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCCentralManager.h"
|
||||
#import "internal/platform/implementation/apple/Tests/GNCFakePeripheral.h"
|
||||
|
||||
@implementation GNCFakeCentralManager {
|
||||
CBManagerState _state;
|
||||
NSArray<CBUUID *> *_serviceUUIDs;
|
||||
}
|
||||
|
||||
@synthesize centralDelegate;
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_state = CBManagerStateUnknown;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CBManagerState)state {
|
||||
return _state;
|
||||
}
|
||||
|
||||
- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs
|
||||
options:(nullable NSDictionary<NSString *, id> *)options {
|
||||
_serviceUUIDs = serviceUUIDs;
|
||||
}
|
||||
|
||||
- (void)connectPeripheral:(id<GNCPeripheral>)peripheral
|
||||
options:(nullable NSDictionary<NSString *, id> *)options {
|
||||
if (_didFailToConnectPeripheralError) {
|
||||
[centralDelegate gnc_centralManager:self
|
||||
didFailToConnectPeripheral:peripheral
|
||||
error:_didFailToConnectPeripheralError];
|
||||
return;
|
||||
}
|
||||
[centralDelegate gnc_centralManager:self didConnectPeripheral:peripheral];
|
||||
}
|
||||
|
||||
- (void)stopScan {
|
||||
}
|
||||
|
||||
#pragma mark - Testing Helpers
|
||||
|
||||
- (NSArray<CBUUID *> *)serviceUUIDs {
|
||||
return _serviceUUIDs;
|
||||
}
|
||||
|
||||
- (void)simulateCentralManagerDidUpdateState:(CBManagerState)fakeState {
|
||||
_state = fakeState;
|
||||
[centralDelegate gnc_centralManagerDidUpdateState:self];
|
||||
}
|
||||
|
||||
- (void)simulateCentralManagerDidDiscoverPeripheral:(id<GNCPeripheral>)peripheral
|
||||
advertisementData:
|
||||
(NSDictionary<NSString *, id> *)advertisementData {
|
||||
[centralDelegate gnc_centralManager:self
|
||||
didDiscoverPeripheral:peripheral
|
||||
advertisementData:advertisementData
|
||||
RSSI:[NSNumber numberWithInt:0]];
|
||||
}
|
||||
|
||||
- (void)simulateCentralManagerDidDisconnectPeripheral:(id<GNCPeripheral>)peripheral {
|
||||
[centralDelegate gnc_centralManager:self didDisconnectPeripheral:peripheral error:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -38,6 +38,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation GNCFakePeripheral {
|
||||
NSMutableArray<CBService *> *_services;
|
||||
NSUUID *_identifier;
|
||||
}
|
||||
|
||||
@synthesize peripheralDelegate;
|
||||
@@ -46,10 +47,15 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_services = [[NSMutableArray alloc] init];
|
||||
_identifier = [[NSUUID alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSUUID *)identifier {
|
||||
return _identifier;
|
||||
}
|
||||
|
||||
- (nullable NSArray<CBService *> *)services {
|
||||
return _services;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
- (void)testEncodingWithPadding {
|
||||
NSString *expected = @"AQ";
|
||||
NSData *data = [[NSData alloc] initWithBase64EncodedString:@"AQ==" options:0];
|
||||
NSString *actual = [data webSafebase64EncodedString];
|
||||
NSString *actual = [data webSafeBase64EncodedString];
|
||||
XCTAssertEqualObjects(expected, actual);
|
||||
}
|
||||
|
||||
@@ -35,8 +35,58 @@
|
||||
[[NSData alloc] initWithBase64EncodedString:
|
||||
@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
options:0];
|
||||
NSString *actual = [data webSafebase64EncodedString];
|
||||
NSString *actual = [data webSafeBase64EncodedString];
|
||||
XCTAssertEqualObjects(expected, actual);
|
||||
}
|
||||
|
||||
- (void)testDecodingWithoutPadding {
|
||||
NSData *expected = [[NSData alloc] initWithBase64EncodedString:@"AQ==" options:0];
|
||||
NSData *actual = [[NSData alloc] initWithWebSafeBase64EncodedString:@"AQ"];
|
||||
XCTAssertNotNil(actual);
|
||||
XCTAssertEqualObjects(expected, actual);
|
||||
}
|
||||
|
||||
- (void)testDecodingNoPad {
|
||||
NSData *expected = [[NSData alloc] initWithBase64EncodedString:@"aaaa" options:0];
|
||||
NSData *actual = [[NSData alloc] initWithWebSafeBase64EncodedString:@"aaaa"];
|
||||
XCTAssertNotNil(actual);
|
||||
XCTAssertEqualObjects(expected, actual);
|
||||
}
|
||||
|
||||
- (void)testDecoding1Pad {
|
||||
NSData *expected = [[NSData alloc] initWithBase64EncodedString:@"aaa=" options:0];
|
||||
NSData *actual = [[NSData alloc] initWithWebSafeBase64EncodedString:@"aaa"];
|
||||
XCTAssertNotNil(actual);
|
||||
XCTAssertEqualObjects(expected, actual);
|
||||
}
|
||||
|
||||
- (void)testDecoding2Pad {
|
||||
NSData *expected = [[NSData alloc] initWithBase64EncodedString:@"aa==" options:0];
|
||||
NSData *actual = [[NSData alloc] initWithWebSafeBase64EncodedString:@"aa"];
|
||||
XCTAssertNotNil(actual);
|
||||
XCTAssertEqualObjects(expected, actual);
|
||||
}
|
||||
|
||||
- (void)testDecodingSingleCharacterInLastQuadruple {
|
||||
NSData *actual = [[NSData alloc] initWithWebSafeBase64EncodedString:@"aaaab"];
|
||||
XCTAssertNil(actual);
|
||||
}
|
||||
|
||||
- (void)testDecodingWithAllValidCharacters {
|
||||
NSData *expected =
|
||||
[[NSData alloc] initWithBase64EncodedString:
|
||||
@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
options:0];
|
||||
NSData *actual =
|
||||
[[NSData alloc] initWithWebSafeBase64EncodedString:
|
||||
@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"];
|
||||
XCTAssertNotNil(actual);
|
||||
XCTAssertEqualObjects(expected, actual);
|
||||
}
|
||||
|
||||
- (void)testDecodingWithIllegalCharacters {
|
||||
NSData *actual = [[NSData alloc] initWithWebSafeBase64EncodedString:@"@#$^&*()"];
|
||||
XCTAssertNil(actual);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Reference in New Issue
Block a user