From 0f8e5283a4f26aaa35d8c7e37dc2942fe2762d65 Mon Sep 17 00:00:00 2001 From: Nick Bourdakos Date: Tue, 18 Jul 2023 12:36:03 -0700 Subject: [PATCH] Add GATT server PiperOrigin-RevId: 549080231 --- .../apple/Mediums/BLEv2/GNCBLEGATTServer.h | 82 ++++ .../apple/Mediums/BLEv2/GNCBLEGATTServer.m | 300 +++++++++++ .../Mediums/BLEv2/GNCPeripheralManager.h | 190 +++++++ .../Mediums/BLEv2/GNCPeripheralManager.m | 37 ++ .../Mediums/BLEv2/NSData+GNCWebSafeBase64.h | 26 + .../Mediums/BLEv2/NSData+GNCWebSafeBase64.m | 34 ++ .../implementation/apple/Mediums/BUILD | 7 + .../platform/implementation/apple/Tests/BUILD | 5 + .../apple/Tests/GNCBLEGATTServer+Testing.h | 36 ++ .../apple/Tests/GNCBLEGATTServerTest.m | 464 ++++++++++++++++++ .../apple/Tests/GNCFakePeripheralManager.h | 77 +++ .../apple/Tests/GNCFakePeripheralManager.m | 155 ++++++ .../apple/Tests/NSData+GNCWebSafeBase64Test.m | 42 ++ 13 files changed, 1455 insertions(+) create mode 100644 internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h create mode 100644 internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.m create mode 100644 internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheralManager.h create mode 100644 internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheralManager.m create mode 100644 internal/platform/implementation/apple/Mediums/BLEv2/NSData+GNCWebSafeBase64.h create mode 100644 internal/platform/implementation/apple/Mediums/BLEv2/NSData+GNCWebSafeBase64.m create mode 100644 internal/platform/implementation/apple/Tests/GNCBLEGATTServer+Testing.h create mode 100644 internal/platform/implementation/apple/Tests/GNCBLEGATTServerTest.m create mode 100644 internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.h create mode 100644 internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.m create mode 100644 internal/platform/implementation/apple/Tests/NSData+GNCWebSafeBase64Test.m diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h new file mode 100644 index 00000000..9131555d --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h @@ -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 +#import + +@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 *)serviceData; + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.m b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.m new file mode 100644 index 00000000..e80fec94 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.m @@ -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 +#import + +#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 () +@end + +@implementation GNCBLEGATTServer { + id _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 *_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 *_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 *_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)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 *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 *)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)peripheral { + [_condition lock]; + [_condition signal]; + [_condition unlock]; +} + +- (void)gnc_peripheralManagerDidStartAdvertising:(id)peripheral + error:(nullable NSError *)error { + [_condition lock]; + [_condition signal]; + [_condition unlock]; +} + +- (void)gnc_peripheralManager:(id)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)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 diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheralManager.h b/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheralManager.h new file mode 100644 index 00000000..9ea367e8 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheralManager.h @@ -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 +#import + +@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 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 you’ve 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 doesn’t 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, there’s 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 don’t 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 + * don’t 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 isn’t 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 *)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 + * characteristic’s 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 + +/** + * Tells the delegate the peripheral manager’s 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)peripheral; + +/** + * Tells the delegate the peripheral manager started advertising the local peripheral device’s data. + * + * Called when your app calls the @c startAdvertising: method to advertise the local peripheral + * device’s 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)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 + * peripheral’s 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)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 characteristic’s + * value. + */ +- (void)gnc_peripheralManager:(id)peripheral + didReceiveReadRequest:(CBATTRequest *)request; + +@end + +@interface CBPeripheralManager () +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheralManager.m b/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheralManager.m new file mode 100644 index 00000000..cf7eecbd --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheralManager.m @@ -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 + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation CBPeripheralManager (GNCPeripheralManagerAdditions) + +- (void)setPeripheralDelegate:(nullable id)peripheralDelegate { + NSAssert([peripheralDelegate conformsToProtocol:@protocol(CBPeripheralManagerDelegate)], + @"peripheralDelegate must conform to protocol CBPeripheralManagerDelegate"); + self.delegate = (id)peripheralDelegate; +} + +- (nullable id)peripheralDelegate { + return (id)self.delegate; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/NSData+GNCWebSafeBase64.h b/internal/platform/implementation/apple/Mediums/BLEv2/NSData+GNCWebSafeBase64.h new file mode 100644 index 00000000..c4d03b74 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLEv2/NSData+GNCWebSafeBase64.h @@ -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 + +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 diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/NSData+GNCWebSafeBase64.m b/internal/platform/implementation/apple/Mediums/BLEv2/NSData+GNCWebSafeBase64.m new file mode 100644 index 00000000..1f0b3014 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLEv2/NSData+GNCWebSafeBase64.m @@ -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 diff --git a/internal/platform/implementation/apple/Mediums/BUILD b/internal/platform/implementation/apple/Mediums/BUILD index 43b4056e..d1a70ace 100644 --- a/internal/platform/implementation/apple/Mediums/BUILD +++ b/internal/platform/implementation/apple/Mediums/BUILD @@ -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", ], ) diff --git a/internal/platform/implementation/apple/Tests/BUILD b/internal/platform/implementation/apple/Tests/BUILD index 4a480bde..8f4ee847 100644 --- a/internal/platform/implementation/apple/Tests/BUILD +++ b/internal/platform/implementation/apple/Tests/BUILD @@ -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", diff --git a/internal/platform/implementation/apple/Tests/GNCBLEGATTServer+Testing.h b/internal/platform/implementation/apple/Tests/GNCBLEGATTServer+Testing.h new file mode 100644 index 00000000..a125da19 --- /dev/null +++ b/internal/platform/implementation/apple/Tests/GNCBLEGATTServer+Testing.h @@ -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 + +@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)peripheralManager; + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Tests/GNCBLEGATTServerTest.m b/internal/platform/implementation/apple/Tests/GNCBLEGATTServerTest.m new file mode 100644 index 00000000..f0531320 --- /dev/null +++ b/internal/platform/implementation/apple/Tests/GNCBLEGATTServerTest.m @@ -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 +#import +#import + +#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 *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 *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 *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 *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 *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 *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 diff --git a/internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.h b/internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.h new file mode 100644 index 00000000..b1af4edb --- /dev/null +++ b/internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.h @@ -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 +#import + +#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 + +/** + * 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 *services; + +/** The data being advertised. */ +@property(nonatomic, nullable, readonly) NSDictionary *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 diff --git a/internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.m b/internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.m new file mode 100644 index 00000000..96cbd8ea --- /dev/null +++ b/internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.m @@ -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 +#import +#import + +#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 *_advertisementData; + NSMutableArray *_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 *)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 *)services { + return _services; +} + +- (NSDictionary *)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 diff --git a/internal/platform/implementation/apple/Tests/NSData+GNCWebSafeBase64Test.m b/internal/platform/implementation/apple/Tests/NSData+GNCWebSafeBase64Test.m new file mode 100644 index 00000000..ec1c14b6 --- /dev/null +++ b/internal/platform/implementation/apple/Tests/NSData+GNCWebSafeBase64Test.m @@ -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 +#import + +@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