Add BLE Medium

PiperOrigin-RevId: 557933813
This commit is contained in:
Nick Bourdakos
2023-08-17 14:17:28 -07:00
committed by Copybara-Service
parent 07a94a950d
commit 7933842c29
19 changed files with 1250 additions and 17 deletions
@@ -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 peripherals 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 peripherals 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 peripherals 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 delegates @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 dont 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 managers state updated.
*
* You implement this required method to ensure that the central device supports Bluetooth low
* energy and that its available to use. You should issue commands to the central manager only when
* the central managers @c state indicates its 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 peripherals 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 dont 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 peripherals 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