Add GATT server

PiperOrigin-RevId: 549080231
This commit is contained in:
Nick Bourdakos
2023-07-18 12:37:23 -07:00
committed by Copybara-Service
parent 99053e4df3
commit 0f8e5283a4
13 changed files with 1455 additions and 0 deletions
@@ -0,0 +1,82 @@
// 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 GNCBLEGATTCharacteristic;
NS_ASSUME_NONNULL_BEGIN
/**
* An object that manages and advertises GATT characteritics.
*
* @note The public APIs of this class are NOT thread safe. All methods in this class should be
* invoked from the same thread or serially.
*/
@interface GNCBLEGATTServer : NSObject
/**
* Creates a characteristic and adds it to the GATT server.
*
* Characteristics of the same service UUID will cause the service to be unpublished and republished
* with the new characteristic appended.
*
* This method blocks until the characteristic has successfully been added to the GATT server or an
* error occurs.
*
* @param serviceUUID A 128-bit UUID that identifies the service that the characteristic belongs to.
* @param characteristicUUID A 128-bit UUID that identifies the characteristic.
* @param permissions The permissions of the characteristic value.
* @param properties The properties of the characteristic.
* @return Returns the characteristic or nil if an error has occured.
*/
- (nullable GNCBLEGATTCharacteristic *)
createCharacteristicWithServiceID:(CBUUID *)serviceUUID
characteristicUUID:(CBUUID *)characteristicUUID
permissions:(CBAttributePermissions)permissions
properties:(CBCharacteristicProperties)properties;
/**
* Updates a local characteristic with the provided value.
*
* @param characteristic The characteristic to update.
* @param value The new value for the characteristic.
* @return Returns @c YES if successfully updated or @c NO if an error has occured.
*/
- (BOOL)updateCharacteristic:(GNCBLEGATTCharacteristic *)characteristic
value:(nullable NSData *)value;
/** Removes all published services from the local GATT database. */
- (void)stop;
/**
* 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 method blocks until the service data is being advertised or an error occurs.
*
* @param serviceData A dictionary that contains service-specific advertisement data.
* @return Returns @c YES if successfully updated or @c NO if an error has occured.
*/
- (BOOL)startAdvertisingData:(NSDictionary<CBUUID *, NSData *> *)serviceData;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,300 @@
// 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/GNCBLEGATTServer.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheralManager.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/NSData+GNCWebSafeBase64.h"
NS_ASSUME_NONNULL_BEGIN
// An arbitrary timeout that should be pretty lenient.
static NSTimeInterval const GNCBLEGATTServerTimeoutInSeconds = 2;
@interface GNCBLEGATTServer () <GNCPeripheralManagerDelegate, CBPeripheralManagerDelegate>
@end
@implementation GNCBLEGATTServer {
id<GNCPeripheralManager> _peripheralManager;
// A map of service UUIDs to the service. This is used to keep track of all services and
// characteristics that should be in the GATT database. This also keeps track of a characteristics
// dynamic read value. This is the value returned when a remote device attempts to read the
// characteristic.
NSMutableDictionary<CBUUID *, CBMutableService *> *_services;
// A list of services that have been requested to be added to the GATT database, but have not yet
// completed. This is used by the create characteristic method to block until the characteristic
// has been added to the database.
NSMutableArray<CBUUID *> *_pendingServiceAdditions;
// A map of service UUIDs to its associated error if the service had failed to be added to the
// GATT database. This is used by the create characteristic method to determine if the service was
// successfully added.
NSMutableDictionary<CBUUID *, NSError *> *_serviceErrors;
// Guards access to @c _services, @c _pendingServiceAdditions and @c _serviceErrors. The condition
// is also used to block method execution until its async action completes.
NSCondition *_condition;
}
- (instancetype)init {
dispatch_queue_t queue =
dispatch_queue_create("com.nearby.GNCBLEGATTServer", DISPATCH_QUEUE_SERIAL);
return [self initWithPeripheralManager:[[CBPeripheralManager alloc] initWithDelegate:nil
queue:queue]];
}
- (instancetype)initWithPeripheralManager:(id<GNCPeripheralManager>)peripheralManager {
self = [super init];
if (self) {
_peripheralManager = peripheralManager;
// Set for @c GNCPeripheralManager to be able to forward callbacks.
_peripheralManager.peripheralDelegate = self;
_services = [[NSMutableDictionary alloc] init];
_pendingServiceAdditions = [[NSMutableArray alloc] init];
_serviceErrors = [[NSMutableDictionary alloc] init];
_condition = [[NSCondition alloc] init];
}
return self;
};
- (nullable GNCBLEGATTCharacteristic *)
createCharacteristicWithServiceID:(CBUUID *)serviceUUID
characteristicUUID:(CBUUID *)characteristicUUID
permissions:(CBAttributePermissions)permissions
properties:(CBCharacteristicProperties)properties {
// Ensure we are in a powered on state.
if (![self waitUntilPoweredOn]) {
return nil;
}
NSDate *timeLimit = [NSDate dateWithTimeIntervalSinceNow:GNCBLEGATTServerTimeoutInSeconds];
BOOL wasSignaled = YES;
[_condition lock];
// If a service with the specified UUID exists, we are modifying it, which requires us to remove
// the service and then re-add it after the new characteristic has been added. Otherwise, create
// the service for the specified UUID and keep track of it.
CBMutableService *service = _services[serviceUUID];
if (service != nil) {
[_peripheralManager removeService:service];
} else {
service = [[CBMutableService alloc] initWithType:serviceUUID primary:YES];
_services[serviceUUID] = service;
}
// Create a characteristic with the specified permissions and properties.
CBMutableCharacteristic *characteristic =
[[CBMutableCharacteristic alloc] initWithType:characteristicUUID
properties:properties
value:nil
permissions:permissions];
// Add the characteristic to the service's list of characteristics
NSMutableArray<CBMutableCharacteristic *> *characteristics =
[service.characteristics mutableCopy];
if (characteristics == nil) {
characteristics = [[NSMutableArray alloc] init];
}
[characteristics addObject:characteristic];
service.characteristics = characteristics;
// Publish the service and wait until complete.
[_pendingServiceAdditions addObject:service.UUID];
[_peripheralManager addService:service];
while ([_pendingServiceAdditions containsObject:service.UUID] && wasSignaled) {
wasSignaled = [_condition waitUntilDate:timeLimit];
}
NSError *serviceError = [_serviceErrors objectForKey:service.UUID];
[_serviceErrors removeObjectForKey:service.UUID];
[_condition unlock];
if (serviceError) {
return nil;
}
return [[GNCBLEGATTCharacteristic alloc] initWithUUID:characteristicUUID
serviceUUID:serviceUUID
permissions:permissions
properties:properties];
}
- (BOOL)updateCharacteristic:(GNCBLEGATTCharacteristic *)characteristic
value:(nullable NSData *)value {
// Ensure we are in a powered on state.
if (![self waitUntilPoweredOn]) {
return NO;
}
[_condition lock];
// Find and update the specified characteristic's value.
CBMutableService *service = _services[characteristic.serviceUUID];
if (!service) {
[_condition unlock];
return NO;
}
for (CBMutableCharacteristic *c in service.characteristics) {
if ([c.UUID isEqual:characteristic.characteristicUUID]) {
c.value = value;
[_condition unlock];
return YES;
}
}
[_condition unlock];
return NO;
}
- (void)stop {
[_condition lock];
[_services removeAllObjects];
[_condition unlock];
[_peripheralManager removeAllServices];
}
- (BOOL)startAdvertisingData:(NSDictionary<CBUUID *, NSData *> *)serviceData {
// We can only handle advertising a single service data item, so return early if there is more
// than one service incuded.
if (serviceData.count > 1) {
return NO;
}
// Ensure we are in a powered on state.
if (![self waitUntilPoweredOn]) {
return NO;
}
if (serviceData.count == 1) {
// Apple doesn't support setting service data, so we must convert it to a "local name". We do
// this by assuming there will only ever be one service and then base64 encoding its associated
// data. Other platforms are aware of this behavior and always check the local name if service
// data is unavailable.
CBUUID *serviceUUID = [serviceData.allKeys objectAtIndex:0];
NSData *value = [serviceData objectForKey:serviceUUID];
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.
if (encoded.length > 22) {
encoded = [encoded substringToIndex:22];
}
[_peripheralManager startAdvertising:@{
CBAdvertisementDataLocalNameKey : encoded,
CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ]
}];
} else {
[_peripheralManager startAdvertising:nil];
}
// Wait until advertisement has started.
NSDate *timeLimit = [NSDate dateWithTimeIntervalSinceNow:GNCBLEGATTServerTimeoutInSeconds];
BOOL wasSignaled = YES;
[_condition lock];
while (!_peripheralManager.isAdvertising && wasSignaled) {
wasSignaled = [_condition waitUntilDate:timeLimit];
}
[_condition unlock];
return _peripheralManager.isAdvertising;
}
#pragma mark - Helpers
- (BOOL)waitUntilPoweredOn {
NSDate *timeLimit = [NSDate dateWithTimeIntervalSinceNow:GNCBLEGATTServerTimeoutInSeconds];
BOOL timedOut = NO;
[_condition lock];
while (_peripheralManager.state != CBManagerStatePoweredOn && !timedOut) {
timedOut = ![_condition waitUntilDate:timeLimit];
}
[_condition unlock];
return _peripheralManager.state == CBManagerStatePoweredOn;
}
#pragma mark - GNCPeripheralManagerDelegate
- (void)gnc_peripheralManagerDidUpdateState:(id<GNCPeripheralManager>)peripheral {
[_condition lock];
[_condition signal];
[_condition unlock];
}
- (void)gnc_peripheralManagerDidStartAdvertising:(id<GNCPeripheralManager>)peripheral
error:(nullable NSError *)error {
[_condition lock];
[_condition signal];
[_condition unlock];
}
- (void)gnc_peripheralManager:(id<GNCPeripheralManager>)peripheral
didAddService:(CBService *)service
error:(nullable NSError *)error {
[_condition lock];
[_pendingServiceAdditions removeObject:service.UUID];
if (error) {
[_serviceErrors setObject:error forKey:service.UUID];
}
[_condition signal];
[_condition unlock];
}
- (void)gnc_peripheralManager:(id<GNCPeripheralManager>)peripheral
didReceiveReadRequest:(CBATTRequest *)request {
[_condition lock];
// Find the requested charactertic and respond with its value if it exists.
CBMutableService *service = _services[request.characteristic.service.UUID];
if (!service) {
[_condition unlock];
[_peripheralManager respondToRequest:request withResult:CBATTErrorAttributeNotFound];
return;
}
for (CBMutableCharacteristic *c in service.characteristics) {
if ([c.UUID isEqual:request.characteristic.UUID]) {
request.value = c.value;
[_condition unlock];
[_peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
return;
}
}
[_condition unlock];
[_peripheralManager respondToRequest:request withResult:CBATTErrorAttributeNotFound];
}
#pragma mark - CBPeripheralManagerDelegate
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
[self gnc_peripheralManagerDidUpdateState:peripheral];
}
- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral
error:(nullable NSError *)error {
[self gnc_peripheralManagerDidStartAdvertising:peripheral error:error];
}
- (void)peripheralManager:(CBPeripheralManager *)peripheral
didAddService:(CBService *)service
error:(nullable NSError *)error {
[self gnc_peripheralManager:peripheral didAddService:service error:error];
}
- (void)peripheralManager:(CBPeripheralManager *)peripheral
didReceiveReadRequest:(CBATTRequest *)request {
[self gnc_peripheralManager:peripheral didReceiveReadRequest:request];
}
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,190 @@
// 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 GNCPeripheralManagerDelegate;
NS_ASSUME_NONNULL_BEGIN
/** Protocol which helps create a fake of a @c CBPeripheralManager to inject for testing. */
@protocol GNCPeripheralManager
/** Shadow property of a @c CBPeripheralManagerDelegate. */
@property(nonatomic, nullable) id<GNCPeripheralManagerDelegate> peripheralDelegate;
@property(nonatomic, assign, readonly) CBManagerState state;
@property(nonatomic, assign, readonly) BOOL isAdvertising;
/**
* Publishes a service and any of its associated characteristics and characteristic descriptors to
* the local GATT database.
*
* When you add a service to the database, the peripheral manager calls the
* @c gnc_peripheralManager:didAddService:error: method of its delegate object. If the service
* contains any included services, you must first publish them.
*
* @param service The service you want to publish.
*/
- (void)addService:(CBMutableService *)service;
/**
* Removes a specified published service from the local GATT database.
*
* Because apps on the local peripheral device share the GATT database, more than one instance of a
* service may exist in the database. As a result, this method removes only the instance of the
* service that your app added to the database (using the @c addService: method). If any other
* services contains this service, you must first remove them.
*
* @param service The service you want to remove.
*/
- (void)removeService:(CBMutableService *)service;
/**
* Removes all published services from the local GATT database.
*
* Use this when you want to remove all services youve previously published, for example, if your
* app has a toggle button to expose GATT services.
*
* Because apps on the local peripheral device share the GATT database, this method removes only the
* services that you added using the @c addService: method. This call doesnt remove any services
* published by other apps on the local peripheral device.
*/
- (void)removeAllServices;
/**
* Advertises peripheral manager data.
*
* When you start advertising peripheral data, the peripheral manager calls the
* @c gnc_peripheralManagerDidStartAdvertising:error: method of its delegate object.
*
* Core Bluetooth advertises data on a “best effort” basis, due to limited space and because there
* may be multiple apps advertising simultaneously. While in the foreground, your app can use up to
* 28 bytes of space in the initial advertisement data for any combination of the supported
* advertising data keys. If no this space remains, theres an additional 10 bytes of space in the
* scan response, usable only for the local name (represented by the value of the
* @c CBAdvertisementDataLocalNameKey key). Note that these sizes dont include the 2 bytes of
* header information required for each new data type.
*
* Any service UUIDs contained in the value of the @c CBAdvertisementDataServiceUUIDsKey key that
* dont fit in the allotted space go to a special “overflow” area. These services are discoverable
* only by an iOS device explicitly scanning for them.
*
* While your app is in the background, the local name isnt advertised and all service UUIDs are in
* the overflow area.
*
* For details about the format of advertising and response data, see the Bluetooth 4.0
* specification, Volume 3, Part C, Section 11.
*
* @param advertisementData An optional dictionary containing the data you want to advertise. The
* peripheral manager only supports two keys:
* @c CBAdvertisementDataLocalNameKey and
* @c CBAdvertisementDataServiceUUIDsKey.
*/
- (void)startAdvertising:(nullable NSDictionary<NSString *, id> *)advertisementData;
/**
* Responds to a read or write request from a connected central.
*
* When the peripheral manager receives a request from a connected central to read or write a
* characteristics value, it calls the @c gnc_peripheralManager:didReceiveReadRequest: or
* @c gnc_peripheralManager:didReceiveWriteRequests: method of its delegate object. To respond to
* the corresponding read or write request, you call this method whenever you recevie one of these
* delegate method callbacks.
*
* @param request The read or write request received from the connected central. For more
* information about read and write requests, see @c CBATTRequest.
* @param result The result of attempting to fulfill the request.
*/
- (void)respondToRequest:(CBATTRequest *)request withResult:(CBATTError)result;
@end
/**
* Protocol which helps the @c GNCPeripheralManager wrap a @c CBPeripheralManagerDelegate for
* testing.
*/
@protocol GNCPeripheralManagerDelegate <NSObject>
/**
* Tells the delegate the peripheral managers state updated.
*
* You implement this required method to ensure that Bluetooth low energy is available to use on the
* local peripheral device.
*
* Issue commands to the peripheral manager only when the peripheral manager is in the powered-on
* state, as indicated by the @c CBPeripheralManagerStatePoweredOn constant. A state with a value
* lower than @c CBPeripheralManagerStatePoweredOn implies that advertising has stopped and that any
* connected centrals have been disconnected. If the state moves below
* @c CBPeripheralManagerStatePoweredOff, advertising has stopped you must explicitly restart it. In
* addition, the powered off state clears the local database; in this case you must explicitly
* re-add all services. For a complete list and discussion of the possible values representing the
* state of the peripheral manager, see the @c CBPeripheralManagerState enumeration in
* @c CBPeripheralManager.
*
* @param peripheral The peripheral manager whose state has changed.
*/
- (void)gnc_peripheralManagerDidUpdateState:(id<GNCPeripheralManager>)peripheral;
/**
* Tells the delegate the peripheral manager started advertising the local peripheral devices data.
*
* Called when your app calls the @c startAdvertising: method to advertise the local peripheral
* devices data. If successful, the @c error parameter is @c nil. If a problem prevents advertising
* the data, the @c error parameter returns the cause of the failure.
*
* @param peripheral The peripheral manager that is starting advertising.
* @param error The reason the call failed, or @c nil if no error occurred.
*/
- (void)gnc_peripheralManagerDidStartAdvertising:(id<GNCPeripheralManager>)peripheral
error:(nullable NSError *)error;
/**
* Tells the delegate the peripheral manager published a service to the local GATT database.
*
* Called when your app calls the @c addService: method to publish a service to the local
* peripherals GATT database. If the service published successfully to the local database, the
* @c error parameter is @c nil. If unsuccessful, the @c error parameter provides the cause of the
* failure.
*
* @param peripheral The peripheral manager adding the service.
* @param service The service added to the local GATT database.
* @param error The reason the call failed, or @c nil if no error occurred.
*/
- (void)gnc_peripheralManager:(id<GNCPeripheralManager>)peripheral
didAddService:(CBService *)service
error:(nullable NSError *)error;
/**
* Tells the delegate that a local peripheral received an Attribute Protocol (ATT) read request for
* a characteristic with a dynamic value.
*
* When you receive this callback, call the @c respondToRequest:withResult: method of the
* @c GNCPeripheralManager class exactly once to respond to the read request.
*
* @param peripheral The peripheral manager that received the request.
* @param request A @c CBATTRequest object that represents a request to read a characteristics
* value.
*/
- (void)gnc_peripheralManager:(id<GNCPeripheralManager>)peripheral
didReceiveReadRequest:(CBATTRequest *)request;
@end
@interface CBPeripheralManager () <GNCPeripheralManager>
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,37 @@
// 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/GNCPeripheralManager.h"
#import <objc/NSObject.h>
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@implementation CBPeripheralManager (GNCPeripheralManagerAdditions)
- (void)setPeripheralDelegate:(nullable id<GNCPeripheralManagerDelegate>)peripheralDelegate {
NSAssert([peripheralDelegate conformsToProtocol:@protocol(CBPeripheralManagerDelegate)],
@"peripheralDelegate must conform to protocol CBPeripheralManagerDelegate");
self.delegate = (id<CBPeripheralManagerDelegate>)peripheralDelegate;
}
- (nullable id<GNCPeripheralManagerDelegate>)peripheralDelegate {
return (id<GNCPeripheralManagerDelegate>)self.delegate;
}
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,26 @@
// 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 <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSData (GNCWebSafeBase64)
/** Creates a Base64 encoded string from the data using websafe characters and no padding. */
- (NSString *)webSafebase64EncodedString;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,34 @@
// 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/NSData+GNCWebSafeBase64.h"
NS_ASSUME_NONNULL_BEGIN
@implementation NSData (GNCWebSafeBase64)
- (NSString *)webSafebase64EncodedString {
NSString *encoded = [self base64EncodedStringWithOptions:0];
// Convert the standard base64 characters to URL safe variants.
encoded = [encoded stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
encoded = [encoded stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
encoded = [encoded stringByReplacingOccurrencesOfString:@"=" withString:@""];
return encoded;
}
@end
NS_ASSUME_NONNULL_END
@@ -19,6 +19,9 @@ objc_library(
name = "Mediums",
srcs = [
"BLEv2/GNCBLEGATTCharacteristic.m",
"BLEv2/GNCBLEGATTServer.m",
"BLEv2/GNCPeripheralManager.m",
"BLEv2/NSData+GNCWebSafeBase64.m",
"Ble/GNCMBleCentral.m",
"Ble/GNCMBleConnection.m",
"Ble/GNCMBlePeripheral.m",
@@ -35,6 +38,9 @@ objc_library(
],
hdrs = [
"BLEv2/GNCBLEGATTCharacteristic.h",
"BLEv2/GNCBLEGATTServer.h",
"BLEv2/GNCPeripheralManager.h",
"BLEv2/NSData+GNCWebSafeBase64.h",
"Ble/GNCMBleCentral.h",
"Ble/GNCMBleConnection.h",
"Ble/GNCMBlePeripheral.h",
@@ -55,6 +61,7 @@ objc_library(
"//third_party/apple_frameworks:CoreBluetooth",
"//third_party/apple_frameworks:Foundation",
"//third_party/apple_frameworks:Network",
"//third_party/apple_frameworks:ObjectiveC",
"//third_party/objective_c/google_toolbox_for_mac:GTM_Logger",
],
)
@@ -24,15 +24,20 @@ objc_library(
testonly = True,
srcs = [
"GNCBLEGATTCharacteristicTest.mm",
"GNCBLEGATTServer+Testing.h",
"GNCBLEGATTServerTest.m",
"GNCBLEUtilsTest.mm",
"GNCBleTest.mm",
"GNCBluetoothAdapterTest.mm",
"GNCCryptoTest.mm",
"GNCFakePeripheralManager.h",
"GNCFakePeripheralManager.m",
"GNCIPAddressTest.mm",
"GNCMultiThreadExecutorTest.mm",
"GNCScheduledExecutorTest.mm",
"GNCSingleThreadExecutorTest.mm",
"GNCUtilsTest.mm",
"NSData+GNCWebSafeBase64Test.m",
],
deps = [
"//internal/platform:base",
@@ -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/GNCBLEGATTServer.h"
#import <Foundation/Foundation.h>
@protocol GNCPeripheralManager;
NS_ASSUME_NONNULL_BEGIN
@interface GNCBLEGATTServer (Testing)
/**
* Creates a GATT server with a provided peripheral manager.
*
* This is only exposed for testing and can be used to inject a fake peripheral manager.
*
* @param peripheralManager The peripheral manager instance.
*/
- (instancetype)initWithPeripheralManager:(id<GNCPeripheralManager>)peripheralManager;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,464 @@
// 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/GNCBLEGATTCharacteristic.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/Tests/GNCBLEGATTServer+Testing.h"
#import "internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.h"
static NSString *const kServiceUUID1 = @"0000FEF3-0000-1000-8000-00805F9B34FB";
static NSString *const kServiceUUID2 = @"0000FEF4-0000-1000-8000-00805F9B34FB";
static NSString *const kCharacteristicUUID1 = @"00000000-0000-3000-8000-000000000000";
static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-000000000001";
@interface GNCBLEGATTServerTest : XCTestCase
@end
@implementation GNCBLEGATTServerTest
#pragma mark - Create Characteristic
- (void)testCreateCharacteristic {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
GNCBLEGATTCharacteristic *characteristic =
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
XCTAssertNotNil(characteristic);
XCTAssertEqual(fakePeripheralManager.services.count, 1);
XCTAssertEqual(fakePeripheralManager.services[0].characteristics.count, 1);
}
- (void)testCreateMultipleCharacteristicsForOneService {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID1 = [CBUUID UUIDWithString:kCharacteristicUUID1];
CBUUID *characteristicUUID2 = [CBUUID UUIDWithString:kCharacteristicUUID2];
GNCBLEGATTCharacteristic *characteristic1 =
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID1
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
GNCBLEGATTCharacteristic *characteristic2 =
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID2
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
XCTAssertNotNil(characteristic1);
XCTAssertNotNil(characteristic2);
XCTAssertEqual(fakePeripheralManager.services.count, 1);
XCTAssertEqual(fakePeripheralManager.services[0].characteristics.count, 2);
}
- (void)testCreateCharacteristicNotPoweredOn {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
GNCBLEGATTCharacteristic *characteristic =
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
XCTAssertNil(characteristic);
}
- (void)testCreateCharacteristicServiceFailure {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
fakePeripheralManager.didAddServiceError = [NSError errorWithDomain:@"fake" code:0 userInfo:nil];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
GNCBLEGATTCharacteristic *characteristic =
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
XCTAssertNil(characteristic);
}
#pragma mark - Update Characteristic
- (void)testUpdateCharacteristic {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
GNCBLEGATTCharacteristic *characteristic =
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
BOOL success = [gattServer updateCharacteristic:characteristic value:[NSData data]];
XCTAssertTrue(success);
XCTAssertEqual(fakePeripheralManager.services.count, 1);
XCTAssertEqual(fakePeripheralManager.services[0].characteristics.count, 1);
}
- (void)testUpdateCharacteristicInvalidService {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
GNCBLEGATTCharacteristic *characteristic =
[[GNCBLEGATTCharacteristic alloc] initWithUUID:characteristicUUID serviceUUID:serviceUUID];
BOOL success = [gattServer updateCharacteristic:characteristic value:[NSData data]];
XCTAssertFalse(success);
}
- (void)testUpdateCharacteristicInvalidCharacteristic {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID1 = [CBUUID UUIDWithString:kCharacteristicUUID1];
CBUUID *characteristicUUID2 = [CBUUID UUIDWithString:kCharacteristicUUID2];
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID1
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
GNCBLEGATTCharacteristic *characteristic2 =
[[GNCBLEGATTCharacteristic alloc] initWithUUID:characteristicUUID2 serviceUUID:serviceUUID];
BOOL success = [gattServer updateCharacteristic:characteristic2 value:[NSData data]];
XCTAssertFalse(success);
}
- (void)testUpdateCharacteristicNotPoweredOn {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
GNCBLEGATTCharacteristic *characteristic =
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
BOOL success = [gattServer updateCharacteristic:characteristic value:[NSData data]];
XCTAssertFalse(success);
}
#pragma mark - Read Request
- (void)testReadRequest {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
GNCBLEGATTCharacteristic *characteristic =
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
[gattServer updateCharacteristic:characteristic value:[NSData data]];
[fakePeripheralManager
simulatePeripheralManagerDidReceiveReadRequestForService:serviceUUID
characteristic:characteristicUUID];
[self waitForExpectations:@[ fakePeripheralManager.respondToRequestSuccessExpectation ]
timeout:0];
}
- (void)testReadRequestInvalidService {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
GNCBLEGATTCharacteristic *characteristic =
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
[gattServer updateCharacteristic:characteristic value:[NSData data]];
CBUUID *invalidServiceUUID = [CBUUID UUIDWithString:kServiceUUID2];
[fakePeripheralManager
simulatePeripheralManagerDidReceiveReadRequestForService:invalidServiceUUID
characteristic:characteristicUUID];
[self waitForExpectations:@[ fakePeripheralManager.respondToRequestErrorExpectation ] timeout:0];
}
- (void)testReadRequestInvalidCharacteristic {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
GNCBLEGATTCharacteristic *characteristic =
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
[gattServer updateCharacteristic:characteristic value:[NSData data]];
CBUUID *invalidCharacteristicUUID =
[CBUUID UUIDWithString:kCharacteristicUUID2];
[fakePeripheralManager
simulatePeripheralManagerDidReceiveReadRequestForService:serviceUUID
characteristic:invalidCharacteristicUUID];
[self waitForExpectations:@[ fakePeripheralManager.respondToRequestErrorExpectation ] timeout:0];
}
#pragma mark - Stop
- (void)testStop {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[gattServer stop];
XCTAssertEqual(fakePeripheralManager.services.count, 0);
}
#pragma mark - Start Advertising
- (void)testStartAdvertisingNoServiceData {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
BOOL success = [gattServer startAdvertisingData:@{}];
XCTAssertTrue(success);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqual(data, nil);
}
- (void)testStartAdvertisingEmptyServiceData {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
BOOL success = [gattServer startAdvertisingData:@{
[CBUUID UUIDWithString:@"FEF3"] : [NSData data],
}];
XCTAssertTrue(success);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey], @"");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF3"]);
}
- (void)testStartAdvertisingShortServiceData {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
BOOL success = [gattServer startAdvertisingData:@{
[CBUUID UUIDWithString:@"FEF3"] : [@"0123" dataUsingEncoding:NSUTF8StringEncoding],
}];
XCTAssertTrue(success);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey], @"MDEyMw");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF3"]);
}
- (void)testStartAdvertising22ByteServiceData {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
BOOL success = [gattServer startAdvertisingData:@{
[CBUUID UUIDWithString:@"FEF3"] : [@"0123456789012345" dataUsingEncoding:NSUTF8StringEncoding],
}];
XCTAssertTrue(success);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey], @"MDEyMzQ1Njc4OTAxMjM0NQ");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF3"]);
}
- (void)testStartAdvertisingLongServiceData {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
BOOL success = [gattServer startAdvertisingData:@{
[CBUUID UUIDWithString:@"FEF3"] :
[@"012345678901234567890123456789" dataUsingEncoding:NSUTF8StringEncoding],
}];
XCTAssertTrue(success);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey], @"MDEyMzQ1Njc4OTAxMjM0NT");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF3"]);
}
- (void)testStartAdvertisingWithEmojiServiceData {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
BOOL success = [gattServer startAdvertisingData:@{
[CBUUID UUIDWithString:@"FEF3"] : [@"😁❤️🤡" dataUsingEncoding:NSUTF8StringEncoding],
}];
XCTAssertTrue(success);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey], @"8J-YgeKdpO-4j_CfpKE");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF3"]);
}
- (void)testStartAdvertisingMultipleServices {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
BOOL success = [gattServer startAdvertisingData:@{
[CBUUID UUIDWithString:@"FEF3"] :
[@"012345678901234567890123456789" dataUsingEncoding:NSUTF8StringEncoding],
[CBUUID UUIDWithString:@"FEF4"] :
[@"012345678901234567890123456789" dataUsingEncoding:NSUTF8StringEncoding],
}];
XCTAssertFalse(success);
XCTAssertFalse(fakePeripheralManager.isAdvertising);
}
- (void)testStartAdvertisingNotPoweredOn {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
BOOL success = [gattServer startAdvertisingData:@{}];
XCTAssertFalse(success);
XCTAssertFalse(fakePeripheralManager.isAdvertising);
}
- (void)testStartAdvertisingStartFailure {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
fakePeripheralManager.didStartAdvertisingError = [NSError errorWithDomain:@"fake"
code:0
userInfo:nil];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
BOOL success = [gattServer startAdvertisingData:@{}];
XCTAssertFalse(success);
XCTAssertFalse(fakePeripheralManager.isAdvertising);
}
@end
@@ -0,0 +1,77 @@
// 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/GNCPeripheralManager.h"
@class XCTestExpectation;
NS_ASSUME_NONNULL_BEGIN
/** A fake implementation of @c GNCPeripheralManager to inject for testing. */
@interface GNCFakePeripheralManager : NSObject <GNCPeripheralManager>
/**
* The list of services added.
*
* Unlike CoreBluetooth, this list will contain duplicates if the same service is added more than
* once.
*/
@property(nonatomic, nullable, readonly) NSArray<CBService *> *services;
/** The data being advertised. */
@property(nonatomic, nullable, readonly) NSDictionary<NSString *, id> *advertisementData;
/** Expectation fulfilled when peripheral responds to a request with success. */
@property(nonatomic, readonly) XCTestExpectation *respondToRequestSuccessExpectation;
/** Expectation fulfilled when peripheral responds to a request with an error. */
@property(nonatomic, readonly) XCTestExpectation *respondToRequestErrorExpectation;
/**
* Similates an @c addService: error.
*
* Setting this error to a value other than @c nil will simulate a failure when calling @c
* addService: and will call the @c gnc_peripheralManager:didAddService:error: delegate method with
* the provided error.
*/
@property(nonatomic, nullable, readwrite) NSError *didAddServiceError;
/**
* Similates a @c startAdvertising: error.
*
* Setting this error to a value other than @c nil will simulate a failure when calling @c
* startAdvertising: and will call the @c gnc_peripheralManagerDidStartAdvertising:error: delegate
* method with the provided error.
*/
@property(nonatomic, nullable, readwrite) NSError *didStartAdvertisingError;
/**
* Simulates a state update event.
*
* Updates the peripheral manager state to the provided value and calls the
* @c gnc_peripheralManagerDidUpdateState: delegate method.
*
* @param fakeState The new state to transition to.
*/
- (void)simulatePeripheralManagerDidUpdateState:(CBManagerState)fakeState;
- (void)simulatePeripheralManagerDidReceiveReadRequestForService:(CBUUID *)service
characteristic:(CBUUID *)characteristic;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,155 @@
// 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/GNCFakePeripheralManager.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheralManager.h"
@interface CBCharacteristic ()
// Change property to readwrite for tests.
@property(weak, readwrite, nonatomic) CBService *service;
@end
@interface CBATTRequest ()
// Change property to readwrite for tests.
@property(readwrite, nonatomic) CBCharacteristic *characteristic;
// Keep a strong reference to the service.
@property(readwrite, nonatomic) CBService *service;
- (instancetype)initWithService:(CBUUID *)service characteristic:(CBUUID *)characteristic;
@end
@implementation CBATTRequest
- (instancetype)initWithService:(CBUUID *)service characteristic:(CBUUID *)characteristic {
self = [super init];
if (self) {
_characteristic = [[CBMutableCharacteristic alloc] initWithType:characteristic
properties:0
value:nil
permissions:0];
_service = [[CBMutableService alloc] initWithType:service primary:YES];
_characteristic.service = _service;
}
return self;
}
@end
@implementation GNCFakePeripheralManager {
CBManagerState _state;
BOOL _isAdvertising;
NSDictionary<NSString *, id> *_advertisementData;
NSMutableArray<CBService *> *_services;
// Used to deliver delegate callbacks.
dispatch_queue_t _queue;
}
@synthesize peripheralDelegate;
- (instancetype)init {
self = [super init];
if (self) {
_respondToRequestSuccessExpectation = [[XCTestExpectation alloc]
initWithDescription:@"Fulfilled when peripheral responds to a request with success."];
_respondToRequestErrorExpectation = [[XCTestExpectation alloc]
initWithDescription:@"Fulfilled when peripheral responds to a request with an error."];
_isAdvertising = NO;
_state = CBManagerStateUnknown;
_advertisementData = nil;
_services = [[NSMutableArray alloc] init];
_queue = dispatch_queue_create("com.nearby.GNCFakePeripheralManager", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (CBManagerState)state {
return _state;
}
- (BOOL)isAdvertising {
return _isAdvertising;
}
- (void)addService:(CBMutableService *)service {
if (!_didAddServiceError) {
[_services addObject:service];
}
dispatch_async(_queue, ^{
[peripheralDelegate gnc_peripheralManager:self didAddService:service error:_didAddServiceError];
});
}
- (void)removeService:(CBMutableService *)service {
[_services removeObject:service];
}
- (void)removeAllServices {
[_services removeAllObjects];
}
- (void)startAdvertising:(NSDictionary<NSString *, id> *)advertisementData {
_isAdvertising = _didStartAdvertisingError == nil;
_advertisementData = advertisementData;
dispatch_async(_queue, ^{
[peripheralDelegate gnc_peripheralManagerDidStartAdvertising:self
error:_didStartAdvertisingError];
});
}
- (void)respondToRequest:(CBATTRequest *)request withResult:(CBATTError)result {
if (result == CBATTErrorSuccess) {
[_respondToRequestSuccessExpectation fulfill];
return;
}
[_respondToRequestErrorExpectation fulfill];
}
#pragma mark - Testing Helpers
- (NSArray<CBService *> *)services {
return _services;
}
- (NSDictionary<NSString *, id> *)advertisementData {
return _advertisementData;
}
- (void)simulatePeripheralManagerDidUpdateState:(CBManagerState)fakeState {
_state = fakeState;
dispatch_async(_queue, ^{
[peripheralDelegate gnc_peripheralManagerDidUpdateState:self];
});
}
- (void)simulatePeripheralManagerDidReceiveReadRequestForService:(CBUUID *)service
characteristic:(CBUUID *)characteristic {
CBATTRequest *request = [[CBATTRequest alloc] initWithService:service
characteristic:characteristic];
dispatch_async(_queue, ^{
[peripheralDelegate gnc_peripheralManager:self didReceiveReadRequest:request];
});
}
@end
@@ -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/NSData+GNCWebSafeBase64.h"
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
@interface NSData_GNCWebSafeBase64Test : XCTestCase
@end
@implementation NSData_GNCWebSafeBase64Test
- (void)testEncodingWithPadding {
NSString *expected = @"AQ";
NSData *data = [[NSData alloc] initWithBase64EncodedString:@"AQ==" options:0];
NSString *actual = [data webSafebase64EncodedString];
XCTAssertEqualObjects(expected, actual);
}
- (void)testEncodingWithNonWebSafeCharacters {
NSString *expected = @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
NSData *data =
[[NSData alloc] initWithBase64EncodedString:
@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
options:0];
NSString *actual = [data webSafebase64EncodedString];
XCTAssertEqualObjects(expected, actual);
}
@end