diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEError.h b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEError.h index 5eab7ec3..eb32fee7 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEError.h +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEError.h @@ -23,4 +23,7 @@ typedef NS_ERROR_ENUM(GNCBLEErrorDomain, GNCBLEError){ GNCBLEErrorDuplicateCharacteristic, GNCBLEErrorInvalidServiceData, GNCBLEErrorAlreadyAdvertising, + GNCBLEErrorInvalidCharacteristic, + GNCBLEErrorAlreadyDiscoveringSpecifiedCharacteristics, + GNCBLEErrorAlreadyReadingCharacteristic, }; diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h index a0e56440..de21587b 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h @@ -15,20 +15,24 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + /** A container for GATT characteristic information. */ @interface GNCBLEGATTCharacteristic : NSObject /** @remark init is not an available initializer. */ -- (nonnull instancetype)init NS_UNAVAILABLE; +- (instancetype)init NS_UNAVAILABLE; /** - * Creates a container for GATT characteristic information. + * Creates a container for GATT characteristic information with properties. * * @param characteristicUUID The UUID of the characteristic. * @param serviceUUID The UUID of the service. + * @param properties The properties of the characteristic. */ -- (instancetype)initWithUUID:(nonnull CBUUID *)characteristicUUID - serviceUUID:(nonnull CBUUID *)serviceUUID; +- (instancetype)initWithUUID:(CBUUID *)characteristicUUID + serviceUUID:(CBUUID *)serviceUUID + properties:(CBCharacteristicProperties)properties; /** * Creates a container for GATT characteristic information with permissions and properties. @@ -38,17 +42,16 @@ * @param permissions The permissions of the characteristic value. * @param properties The properties of the characteristic. */ -- (nonnull instancetype)initWithUUID:(nonnull CBUUID *)characteristicUUID - serviceUUID:(nonnull CBUUID *)serviceUUID - permissions:(CBAttributePermissions)permissions - properties:(CBCharacteristicProperties)properties - NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithUUID:(CBUUID *)characteristicUUID + serviceUUID:(CBUUID *)serviceUUID + permissions:(CBAttributePermissions)permissions + properties:(CBCharacteristicProperties)properties NS_DESIGNATED_INITIALIZER; /** The 128-bit UUID that identifies the characteristic. */ -@property(nonatomic, readonly, nonnull) CBUUID *characteristicUUID; +@property(nonatomic, readonly) CBUUID *characteristicUUID; /** The 128-bit UUID that identifies the service that the characteristic belongs to. */ -@property(nonatomic, readonly, nonnull) CBUUID *serviceUUID; +@property(nonatomic, readonly) CBUUID *serviceUUID; /** * The permissions of the characteristic value. @@ -67,3 +70,5 @@ @property(nonatomic, readonly) CBCharacteristicProperties properties; @end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.m b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.m index de8dea86..6759b417 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.m +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.m @@ -17,10 +17,17 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + @implementation GNCBLEGATTCharacteristic -- (instancetype)initWithUUID:(CBUUID *)characteristicUUID serviceUUID:(CBUUID *)serviceUUID { - return [self initWithUUID:characteristicUUID serviceUUID:serviceUUID permissions:0 properties:0]; +- (instancetype)initWithUUID:(CBUUID *)characteristicUUID + serviceUUID:(CBUUID *)serviceUUID + properties:(CBCharacteristicProperties)properties { + return [self initWithUUID:characteristicUUID + serviceUUID:serviceUUID + permissions:0 + properties:properties]; } - (instancetype)initWithUUID:(CBUUID *)characteristicUUID @@ -38,3 +45,5 @@ } @end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTClient.h b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTClient.h new file mode 100644 index 00000000..c55017fc --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTClient.h @@ -0,0 +1,117 @@ +// 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; + +@protocol GNCPeripheral; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A block to be invoked when a call to + * @c discoverCharacteristicsWithUUIDs:serviceUUID:completionHandler: has completed. + * + * @param error On success, error will be @c nil. Otherwise, the error that occurred. + */ +typedef void (^GNCDiscoverCharacteristicsCompletionHandler)(NSError *_Nullable error); + +/** + * A block to be invoked when a call to @c characteristicWithUUID:serviceUUID:completionHandler: has + * completed. + * + * @param characteristic The characteristic or @c nil if an error occurred. + * @param error On success, error will be @c nil. Otherwise, the error that occurred. + */ +typedef void (^GNCGetCharacteristicCompletionHandler)( + GNCBLEGATTCharacteristic *_Nullable characteristic, NSError *_Nullable error); + +/** + * A block to be invoked when a call to @c readValueForCharacteristic:completionHandler: has + * completed. + * + * @param value The characteristic's value or @c nil if an error occurred. + * @param error On success, error will be @c nil. Otherwise, the error that occurred. + */ +typedef void (^GNCReadCharacteristicValueCompletionHandler)(NSData *_Nullable value, + NSError *_Nullable error); + +/** + * An object that can be used to discover, explore, and interact with GATT services and + * characteristics available on a remote peripheral. + * + * @note The public APIs of this class are thread safe. + */ +@interface GNCBLEGATTClient : NSObject + +/** @remark init is not an available initializer. */ +- (instancetype)init NS_UNAVAILABLE; + +/** + * Initializes the GATT client with a specified peripheral. + * + * @param peripheral The peripheral instance. + */ +- (instancetype)initWithPeripheral:(CBPeripheral *)peripheral; + +/** + * Discovers the specified characteristics of a service. + * + * A successful call to this method means the characteristics can be retreived with + * @c characteristicWithUUID:serviceUUID:completionHandler:. + * + * @param characteristics An array of 128-bit UUID objects of the characteristics that you are + * interested in. + * @param serviceUUID A 128-bit UUID that identifies the service that the characteristics belongs + * to. + * @param completionHandler Called on a private queue with @c nil if all characteristics have + * successfully been discovered or else an error. + */ +- (void)discoverCharacteristicsWithUUIDs:(NSArray *)characteristicUUIDs + serviceUUID:(CBUUID *)serviceUUID + completionHandler: + (nullable GNCDiscoverCharacteristicsCompletionHandler)completionHandler; + +/** + * Retrieves a GATT characteristic. + * + * If you haven’t yet called the @c discoverCharacteristicsWithUUIDs:serviceUUID:completionHandler: + * method to discover the characteristic, or if there was an error in doing so, this method will + * complete with an error. + * + * @param characteristicUUID A 128-bit UUID that identifies the characteristic. + * @param serviceUUID A 128-bit UUID that identifies the service that the characteristic belongs to. + * @param completionHandler Called on a private queue with the characteristic if successful or an + * error if one has occured. + */ +- (void)characteristicWithUUID:(CBUUID *)characteristicUUID + serviceUUID:(CBUUID *)serviceUUID + completionHandler:(nullable GNCGetCharacteristicCompletionHandler)completionHandler; + +/** + * Reads the requested characteristic from the associated remote device. + * + * @param characteristic The characteristic whose value you want to read. + * @param completionHandler Called on a private queue with the characteristics value if successful + * or an error if one has occured. + */ +- (void)readValueForCharacteristic:(GNCBLEGATTCharacteristic *)characteristic + completionHandler: + (nullable GNCReadCharacteristicValueCompletionHandler)completionHandler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTClient.m b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTClient.m new file mode 100644 index 00000000..21871a87 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTClient.m @@ -0,0 +1,299 @@ +// 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/GNCBLEGATTClient.h" + +#import +#import + +#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEError.h" +#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h" +#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h" + +NS_ASSUME_NONNULL_BEGIN + +static char *const kGNCBLEGATTClientQueueLabel = "com.nearby.GNCBLEGATTClient"; + +static NSError *InvalidCharacteristicError() { + return [NSError errorWithDomain:GNCBLEErrorDomain + code:GNCBLEErrorInvalidCharacteristic + userInfo:nil]; +} + +static NSError *AlreadyDiscoveringSpecifiedCharacteristicsError() { + return [NSError errorWithDomain:GNCBLEErrorDomain + code:GNCBLEErrorAlreadyDiscoveringSpecifiedCharacteristics + userInfo:nil]; +} + +static NSError *AlreadyReadingCharacteristicError() { + return [NSError errorWithDomain:GNCBLEErrorDomain + code:GNCBLEErrorAlreadyReadingCharacteristic + userInfo:nil]; +} + +@interface GNCBLEGATTClient () +@end + +@implementation GNCBLEGATTClient { + dispatch_queue_t _queue; + id _peripheral; + + /** + * A map of service UUIDs with each service holding a map of a list of characterisitcs to a + * completion handler. This is used to track the groupings of characteristic discovery requests. + * When all characteristics of a request are discovered, the completion handler is called and + * removed from the map. + */ + NSMutableDictionary *, + GNCDiscoverCharacteristicsCompletionHandler> *> + *_discoverCharacteristicsCompletionHandlers; + + /** + * A service to characteristic to completion handler map. Used to track pending read requests. + * When a characteristic's value has been updated, the completion handler is called and removed + * from the map. + */ + NSMutableDictionary *> + *_readCharacteristicValueCompletionHandlers; +} + +- (instancetype)initWithPeripheral:(CBPeripheral *)peripheral { + return [self + initWithPeripheral:peripheral + queue:dispatch_queue_create(kGNCBLEGATTClientQueueLabel, DISPATCH_QUEUE_SERIAL)]; +}; + +// Private. +- (instancetype)initWithPeripheral:(id)peripheral + queue:(nullable dispatch_queue_t)queue { + self = [super init]; + if (self) { + _queue = queue ?: dispatch_get_main_queue(); + _peripheral = peripheral; + _peripheral.peripheralDelegate = self; + _discoverCharacteristicsCompletionHandlers = [[NSMutableDictionary alloc] init]; + _readCharacteristicValueCompletionHandlers = [[NSMutableDictionary alloc] init]; + } + return self; +}; + +- (void)discoverCharacteristicsWithUUIDs:(NSArray *)characteristicUUIDs + serviceUUID:(CBUUID *)serviceUUID + completionHandler: + (nullable GNCDiscoverCharacteristicsCompletionHandler)completionHandler { + dispatch_async(_queue, ^{ + if (!_discoverCharacteristicsCompletionHandlers[serviceUUID]) { + _discoverCharacteristicsCompletionHandlers[serviceUUID] = [[NSMutableDictionary alloc] init]; + } + // Return an error if we are already discovering the provided characteristics. + if (_discoverCharacteristicsCompletionHandlers[serviceUUID][characteristicUUIDs]) { + if (completionHandler) { + completionHandler(AlreadyDiscoveringSpecifiedCharacteristicsError()); + } + return; + } + // Use the list of characteristic UUIDs as the key. This makes it easy to associate the + // completion handler with the list of characteristics we are waiting for, as well as retrieving + // the complete list of characteristics we need to query for a single service. + _discoverCharacteristicsCompletionHandlers[serviceUUID][characteristicUUIDs] = + completionHandler; + + // Note: A call to @c discoverServices: will always be paired with a delegate call even if the + // service has already been discovered. + [_peripheral discoverServices:@[ serviceUUID ]]; + }); +} + +- (void)characteristicWithUUID:(CBUUID *)characteristicUUID + serviceUUID:(CBUUID *)serviceUUID + completionHandler:(nullable GNCGetCharacteristicCompletionHandler)completionHandler { + dispatch_async(_queue, ^{ + CBCharacteristic *characteristic = [self synchronousCharacteristicWithUUID:characteristicUUID + serviceUUID:serviceUUID]; + if (!characteristic) { + if (completionHandler) { + completionHandler(nil, InvalidCharacteristicError()); + } + return; + } + if (completionHandler) { + completionHandler([[GNCBLEGATTCharacteristic alloc] initWithUUID:characteristic.UUID + serviceUUID:characteristic.service.UUID + properties:characteristic.properties], + nil); + } + }); +} + +- (void)readValueForCharacteristic:(GNCBLEGATTCharacteristic *)characteristic + completionHandler: + (nullable GNCReadCharacteristicValueCompletionHandler)completionHandler { + dispatch_async(_queue, ^{ + CBCharacteristic *cbCharacteristic = + [self synchronousCharacteristicWithUUID:characteristic.characteristicUUID + serviceUUID:characteristic.serviceUUID]; + + if (!cbCharacteristic) { + if (completionHandler) { + completionHandler(nil, InvalidCharacteristicError()); + } + return; + } + + if (!_readCharacteristicValueCompletionHandlers[characteristic.serviceUUID]) { + _readCharacteristicValueCompletionHandlers[characteristic.serviceUUID] = + [[NSMutableDictionary alloc] init]; + } + // Return an error if we are already discovering the provided characteristics. + if (_readCharacteristicValueCompletionHandlers[characteristic.serviceUUID] + [characteristic.characteristicUUID]) { + if (completionHandler) { + completionHandler(nil, AlreadyReadingCharacteristicError()); + } + return; + } + _readCharacteristicValueCompletionHandlers[characteristic.serviceUUID] + [characteristic.characteristicUUID] = + completionHandler; + + [_peripheral readValueForCharacteristic:cbCharacteristic]; + }); +} + +#pragma mark - Internal + +- (CBCharacteristic *)synchronousCharacteristicWithUUID:(CBUUID *)characteristicUUID + serviceUUID:(CBUUID *)serviceUUID { + dispatch_assert_queue(_queue); + for (CBService *service in _peripheral.services) { + if ([service.UUID isEqual:serviceUUID]) { + for (CBCharacteristic *characteristic in service.characteristics) { + if ([characteristic.UUID isEqual:characteristicUUID]) { + return characteristic; + } + } + } + } + return nil; +} + +#pragma mark - GNCPeripheralDelegate + +- (void)gnc_peripheral:(id)peripheral didDiscoverServices:(nullable NSError *)error { + dispatch_assert_queue(_queue); + // TODO(b/295911088): Queue incoming requests by service would allow us to not attempt + // characteristc discovery on all services and to short circuit and call the completion handler if + // there was an error. + for (CBService *service in peripheral.services) { + // Flatten lists of characteristics for a given service into a single list for discovery. + NSArray *> *groupedCharacteristics = + _discoverCharacteristicsCompletionHandlers[service.UUID].allKeys; + if (!groupedCharacteristics) { + continue; + } + NSMutableSet *flattenedCharacteristics = [[NSMutableSet alloc] init]; + for (NSArray *characteristics in groupedCharacteristics) { + [flattenedCharacteristics addObjectsFromArray:characteristics]; + } + + // Note: Since we don't clear @c _discoverCharacteristicsCompletionHandlers until the + // characterististics have been discovered, multiple calls to + // @c discoverService:characteristics:completionHandler: will cause duplicate characteristic + // discovery calls on the same service. We CANNOT clear the pending list until after we actually + // discover the characteristics, because there can be more than 1 service with the same UUID. + // This is a common occurence for Nearby services, so it should not be treated as an edge case. + [_peripheral discoverCharacteristics:[flattenedCharacteristics allObjects] forService:service]; + } +} + +- (void)gnc_peripheral:(id)peripheral + didDiscoverCharacteristicsForService:(CBService *)service + error:(nullable NSError *)error { + dispatch_assert_queue(_queue); + + // Check if each group of characteristics is a subset of the discovered characteristics. If all + // characteristics of the group have been discovered, call the completion handler with success, + // otherwise continue waiting. If the characteristic discovery returns an error, call all pending + // discovery request completion handlers with the error. + NSMutableSet *characteristics = [[NSMutableSet alloc] init]; + for (CBCharacteristic *characteristic in service.characteristics) { + [characteristics addObject:characteristic.UUID]; + } + [_discoverCharacteristicsCompletionHandlers[service.UUID].copy + enumerateKeysAndObjectsUsingBlock:^(NSArray *pendingCharacteristics, + GNCDiscoverCharacteristicsCompletionHandler handler, + BOOL *stop) { + if ([[NSSet setWithArray:pendingCharacteristics] isSubsetOfSet:characteristics]) { + _discoverCharacteristicsCompletionHandlers[service.UUID][pendingCharacteristics] = nil; + handler(nil); + return; + } + + // TODO(b/295911088): Queue incoming requests by service to avoid this issue. + // This could be a race, where @c discoverService:characteristics:completionHandler: is + // called multiple times quickly for the same service. If a request fails, all pending + // requests for the same service are also failed. + if (error) { + _discoverCharacteristicsCompletionHandlers[service.UUID][pendingCharacteristics] = nil; + handler(error); + return; + } + }]; +} + +- (void)gnc_peripheral:(id)peripheral + didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic + error:(nullable NSError *)error { + dispatch_assert_queue(_queue); + NSMutableDictionary *handlers = + _readCharacteristicValueCompletionHandlers[characteristic.service.UUID]; + if (!handlers) { + return; + } + GNCReadCharacteristicValueCompletionHandler handler = handlers[characteristic.UUID]; + handlers[characteristic.UUID] = nil; + if (handler) { + handler(characteristic.value, error); + } +} + +#pragma mark - CBPeripheralDelegate + +- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error { + dispatch_async(_queue, ^{ + [self gnc_peripheral:peripheral didDiscoverServices:error]; + }); +} + +- (void)peripheral:(CBPeripheral *)peripheral + didDiscoverCharacteristicsForService:(CBService *)service + error:(nullable NSError *)error { + dispatch_async(_queue, ^{ + [self gnc_peripheral:peripheral didDiscoverCharacteristicsForService:service error:error]; + }); +} + +- (void)peripheral:(CBPeripheral *)peripheral + didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic + error:(nullable NSError *)error { + dispatch_async(_queue, ^{ + [self gnc_peripheral:peripheral didUpdateValueForCharacteristic:characteristic error:error]; + }); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h index 6408be13..2461d5f6 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h @@ -41,8 +41,8 @@ typedef void (^GNCStartAdvertisingCompletionHandler)(NSError *_Nullable error); * @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. - * @param completionHandler Called on the main queue with the characteristic if successfully created - * or an error if one has occured. + * @param completionHandler Called on a private queue with the characteristic if successfully + * created or an error if one has occured. */ - (void)createCharacteristicWithServiceID:(CBUUID *)serviceUUID characteristicUUID:(CBUUID *)characteristicUUID @@ -56,8 +56,8 @@ typedef void (^GNCStartAdvertisingCompletionHandler)(NSError *_Nullable error); * * @param characteristic The characteristic to update. * @param value The new value for the characteristic. - * @param completionHandler Called on the main queue with @c nil if successfully updated or an error - * if one has occured. + * @param completionHandler Called on a private queue with @c nil if successfully updated or an + * error if one has occured. */ - (void)updateCharacteristic:(GNCBLEGATTCharacteristic *)characteristic value:(nullable NSData *)value @@ -76,8 +76,8 @@ typedef void (^GNCStartAdvertisingCompletionHandler)(NSError *_Nullable error); * longer than 22 bytes. This also means we can only support advertising a single service. * * @param serviceData A dictionary that contains service-specific advertisement data. - * @param completionHandler Called on the main queue with @c nil if successfully started advertising - * or an error if one has occured. + * @param completionHandler Called on a private queue with @c nil if successfully started + * advertising or an error if one has occured. */ - (void)startAdvertisingData:(NSDictionary *)serviceData completionHandler:(nullable GNCStartAdvertisingCompletionHandler)completionHandler; diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.m b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.m index 996c8396..2cd60e63 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.m +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.m @@ -27,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN static char *const kGNCBLEGATTServerQueueLabel = "com.nearby.GNCBLEGATTServer"; -@interface GNCBLEGATTServer () +@interface GNCBLEGATTServer () @end @implementation GNCBLEGATTServer { diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h b/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h new file mode 100644 index 00000000..d3a7a1f8 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h @@ -0,0 +1,167 @@ +// 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 GNCPeripheralDelegate; + +NS_ASSUME_NONNULL_BEGIN + +/** Protocol which helps create a fake of a @c CBPeripheral to inject for testing. */ +@protocol GNCPeripheral + +// This can't be @c delegate, because it would shadow @c CBPeripheral's delegate. +/** + * The peripheral's delegate. + * + * This delegate is only sent CBPeripheralDelegate messages, and the delegate is responsible for + * forwarding those messages to its GNCPeripheralDelegate method implementations. + * + * See: https://developer.apple.com/videos/play/wwdc2018/417/ + */ +@property(weak, nonatomic, nullable) id peripheralDelegate; + +/** + * A list of a peripheral’s discovered services. + * + * Returns an array of services (represented by CBService objects) that successful a call to the + * @c discoverServices: method discovered. If you haven’t yet called the @c discoverServices: method + * to discover the services of the peripheral, or if there was an error in doing so, the value of + * this property is @c nil. + */ +@property(retain, readonly, nullable) NSArray *services; + +/** + * Discovers the specified services of the peripheral. + * + * You can provide an array of CBUUID objects, representing service UUIDs, in the @c serviceUUIDs + * parameter. When you do, the peripheral returns only the services of the peripheral that match the + * provided UUIDs. + * + * @note If the @c serviceUUIDs parameter is @c nil, this method returns all of the peripheral’s + * available services. This is much slower than providing an array of service UUIDs to search for. + * + * When the peripheral discovers one or more services, it calls the + * @c peripheral:didDiscoverServices: method of its delegate object. After a peripheral discovers + * services, you can access them through the peripheral’s @c services property. + * + * @param serviceUUIDs An array of CBUUID objects that you are interested in. Each CBUUID object + * represents a UUID that identifies the type of service you want to discover. + */ +- (void)discoverServices:(nullable NSArray *)serviceUUIDs; + +/** + * Discovers the specified characteristics of a service. + * + * You can provide an array of CBUUID objects, representing characteristic UUIDs, in the + * @c characteristicUUIDs parameter. When you do, the peripheral returns only the characteristics of + * the service that match the provided UUIDs. If the @c characteristicUUIDs parameter is @c nil, + * this method returns all characteristics of the service. + * + * @note If the @c characteristicUUIDs parameter is @c nil, this method returns all of the service’s + * characteristics. This is much slower than providing an array of characteristic UUIDs to search + * for. + * + * When the peripheral discovers one or more characteristics of the specified service, it calls the + * @c peripheral:didDiscoverCharacteristicsForService:error: method of its delegate object. After + * the peripheral discovers the service’s characteristics, you can access them through the service’s + * @c characteristics property. + * + * @param characteristicUUIDs An array of CBUUID objects that you are interested in. Each CBUUID + * object represents a UUID that identifies the type of a characteristic + * you want to discover. + * @param service The service whose characteristics you want to discover. + */ +- (void)discoverCharacteristics:(nullable NSArray *)characteristicUUIDs + forService:(CBService *)service; + +/** + * Retrieves the value of a specified characteristic. + * + * When you call this method to read the value of a characteristic, the peripheral calls the + * @c peripheral:didUpdateValueForCharacteristic:error: method of its delegate object. If the + * peripheral successfully reads the value of the characteristic, you can access it through the + * characteristic’s @c value property. + * + * Not all characteristics have a readable value. You can determine whether a characteristic’s value + * is readable by accessing the relevant properties of the CBCharacteristicProperties enumeration. + * + * @param characteristic The characteristic whose value you want to read. + */ +- (void)readValueForCharacteristic:(CBCharacteristic *)characteristic; + +@end + +/** + * Protocol which helps the @c GNCPeripheral wrap a @c CBPeripheralDelegate for + * testing. + */ +@protocol GNCPeripheralDelegate + +/** + * Tells the delegate that peripheral service discovery succeeded. + * + * Called when your app calls the @c discoverServices: method. If the peripheral successfully + * discovers services, you can access them through the peripheral’s @c services property. If + * successful, the @c error parameter is @c nil. If unsuccessful, the @c error parameter returns the + * cause of the failure. + * + * @param peripheral The peripheral to which the services belong. + * @param error The reason the call failed, or @c nil if no error occurred. + */ +- (void)gnc_peripheral:(id)peripheral didDiscoverServices:(nullable NSError *)error; + +/** + * Tells the delegate that the peripheral found characteristics for a service. + * + * Called when your app calls the @c discoverCharacteristics:forService: method. If the peripheral + * successfully discovers the characteristics of the specified service, you can access them through + * the service’s @c characteristics property. If successful, the @c error parameter is @c nil. If + * unsuccessful, the @c error parameter returns the cause of the failure. + * + * @param peripheral The peripheral providing this information. + * @param service The service to which the characteristics belong. + * @param error The reason the call failed, or @c nil if no error occurred. + */ +- (void)gnc_peripheral:(id)peripheral + didDiscoverCharacteristicsForService:(CBService *)service + error:(nullable NSError *)error; + +/** + * Tells the delegate that retrieving the specified characteristic’s value succeeded, or that the + * characteristic’s value changed. + * + * Called when your app calls the @c readValueForCharacteristic: method. If successful, the @c error + * parameter is @c nil. If unsuccessful, the @c error parameter returns the cause of the failure. + * + * @param peripheral The peripheral providing this information. + * @param characteristic The characteristic containing the value. + * @param error The reason the call failed, or @c nil if no error occurred. + */ +- (void)gnc_peripheral:(id)peripheral + didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic + error:(nullable NSError *)error; + +@end + +/** + * Declares that @c CBPeripheral implements the @c GNCPeripheral protocol. + * + * This allows us to directly use a @c CBPeripheral as a @c GNCPeripheral. + */ +@interface CBPeripheral () +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.m b/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.m new file mode 100644 index 00000000..69923133 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.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/GNCPeripheral.h" + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation CBPeripheral (GNCPeripheralAdditions) + +- (void)setPeripheralDelegate:(nullable id)peripheralDelegate { + self.delegate = peripheralDelegate; +} + +- (nullable id)peripheralDelegate { + return (id)self.delegate; +} + +@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 index 5b0aded4..ecbeb887 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheralManager.h +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheralManager.h @@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol GNCPeripheralManager /** Shadow property of a @c CBPeripheralManagerDelegate. */ -@property(nonatomic, nullable) id peripheralDelegate; +@property(weak, nonatomic, nullable) id peripheralDelegate; @property(nonatomic, assign, readonly) CBManagerState state; @@ -124,7 +124,7 @@ NS_ASSUME_NONNULL_BEGIN * Protocol which helps the @c GNCPeripheralManager wrap a @c CBPeripheralManagerDelegate for * testing. */ -@protocol GNCPeripheralManagerDelegate +@protocol GNCPeripheralManagerDelegate /** * Tells the delegate the peripheral manager’s state updated. diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheralManager.m b/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheralManager.m index cf7eecbd..784b6b76 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheralManager.m +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheralManager.m @@ -23,9 +23,7 @@ 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; + self.delegate = peripheralDelegate; } - (nullable id)peripheralDelegate { diff --git a/internal/platform/implementation/apple/Mediums/BUILD b/internal/platform/implementation/apple/Mediums/BUILD index 459b548c..aef72927 100644 --- a/internal/platform/implementation/apple/Mediums/BUILD +++ b/internal/platform/implementation/apple/Mediums/BUILD @@ -20,7 +20,9 @@ objc_library( srcs = [ "BLEv2/GNCBLEError.m", "BLEv2/GNCBLEGATTCharacteristic.m", + "BLEv2/GNCBLEGATTClient.m", "BLEv2/GNCBLEGATTServer.m", + "BLEv2/GNCPeripheral.m", "BLEv2/GNCPeripheralManager.m", "BLEv2/NSData+GNCWebSafeBase64.m", "Ble/GNCMBleCentral.m", @@ -40,7 +42,9 @@ objc_library( hdrs = [ "BLEv2/GNCBLEError.h", "BLEv2/GNCBLEGATTCharacteristic.h", + "BLEv2/GNCBLEGATTClient.h", "BLEv2/GNCBLEGATTServer.h", + "BLEv2/GNCPeripheral.h", "BLEv2/GNCPeripheralManager.h", "BLEv2/NSData+GNCWebSafeBase64.h", "Ble/GNCMBleCentral.h", diff --git a/internal/platform/implementation/apple/Tests/BUILD b/internal/platform/implementation/apple/Tests/BUILD index a14ce29d..864188a5 100644 --- a/internal/platform/implementation/apple/Tests/BUILD +++ b/internal/platform/implementation/apple/Tests/BUILD @@ -24,12 +24,15 @@ objc_library( testonly = True, srcs = [ "GNCBLEGATTCharacteristicTest.mm", + "GNCBLEGATTClientTest.m", "GNCBLEGATTServer+Testing.h", "GNCBLEGATTServerTest.m", "GNCBLEUtilsTest.mm", "GNCBleTest.mm", "GNCBluetoothAdapterTest.mm", "GNCCryptoTest.mm", + "GNCFakePeripheral.h", + "GNCFakePeripheral.m", "GNCFakePeripheralManager.h", "GNCFakePeripheralManager.m", "GNCIPAddressTest.mm", @@ -40,6 +43,7 @@ objc_library( "NSData+GNCWebSafeBase64Test.m", ], deps = [ + ":GNCBLEGATTClient_Testing", "//internal/platform:base", "//internal/platform/implementation:comm", "//internal/platform/implementation:platform", @@ -54,6 +58,15 @@ objc_library( ], ) +objc_library( + name = "GNCBLEGATTClient_Testing", + hdrs = ["GNCBLEGATTClient+Testing.h"], + deps = [ + "//internal/platform/implementation/apple/Mediums", + "//third_party/apple_frameworks:Foundation", + ], +) + ios_unit_test( name = "PlatformTests", minimum_os_version = IOS_MINIMUM_OS, diff --git a/internal/platform/implementation/apple/Tests/GNCBLEGATTCharacteristicTest.mm b/internal/platform/implementation/apple/Tests/GNCBLEGATTCharacteristicTest.mm index c7b99b88..4aa8c827 100644 --- a/internal/platform/implementation/apple/Tests/GNCBLEGATTCharacteristicTest.mm +++ b/internal/platform/implementation/apple/Tests/GNCBLEGATTCharacteristicTest.mm @@ -26,12 +26,15 @@ - (void)testConvenienceInit { CBUUID *characteristicUUID = [CBUUID UUIDWithString:@"00000000-0000-3000-8000-000000000000"]; CBUUID *serviceUUID = [CBUUID UUIDWithString:@"0000FEF3-0000-1000-8000-00805F9B34FB"]; + CBCharacteristicProperties properties = CBCharacteristicPropertyRead; GNCBLEGATTCharacteristic *characteristic = - [[GNCBLEGATTCharacteristic alloc] initWithUUID:characteristicUUID serviceUUID:serviceUUID]; + [[GNCBLEGATTCharacteristic alloc] initWithUUID:characteristicUUID + serviceUUID:serviceUUID + properties:properties]; XCTAssertEqualObjects(characteristic.characteristicUUID, characteristicUUID); XCTAssertEqualObjects(characteristic.serviceUUID, serviceUUID); XCTAssertEqual(characteristic.permissions, 0); - XCTAssertEqual(characteristic.properties, 0); + XCTAssertEqual(characteristic.properties, properties); } - (void)testInit { diff --git a/internal/platform/implementation/apple/Tests/GNCBLEGATTClient+Testing.h b/internal/platform/implementation/apple/Tests/GNCBLEGATTClient+Testing.h new file mode 100644 index 00000000..a3a52f65 --- /dev/null +++ b/internal/platform/implementation/apple/Tests/GNCBLEGATTClient+Testing.h @@ -0,0 +1,40 @@ +// THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_APPLE_TESTS_GNCBLEGATTCLIENT_TESTING_H_ +// 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/GNCBLEGATTClient.h" + +#import + +@protocol GNCPeripheral; + +NS_ASSUME_NONNULL_BEGIN + +@interface GNCBLEGATTClient (Testing) + +/** + * Creates a GATT client with a provided peripheral. + * + * This is only exposed for testing and can be used to inject a fake peripheral. + * + * @param peripheral The peripheral instance. + * @param queue The queue to run on, this must match the queue that the peripheral's delegate is + * running on. Defaults to the main queue when @c nil. + */ +- (instancetype)initWithPeripheral:(id)peripheral + queue:(nullable dispatch_queue_t)queue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Tests/GNCBLEGATTClientTest.m b/internal/platform/implementation/apple/Tests/GNCBLEGATTClientTest.m new file mode 100644 index 00000000..b53cdef7 --- /dev/null +++ b/internal/platform/implementation/apple/Tests/GNCBLEGATTClientTest.m @@ -0,0 +1,748 @@ +// 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/GNCBLEGATTClient.h" + +#import +#import +#import + +#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h" +#import "internal/platform/implementation/apple/Tests/GNCBLEGATTClient+Testing.h" +#import "internal/platform/implementation/apple/Tests/GNCFakePeripheral.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 GNCBLEGATTClientTest : XCTestCase +@end + +@implementation GNCBLEGATTClientTest + +#pragma mark - Discover Characteristics + +- (void)testDiscoverCharacteristics { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + + XCTestExpectation *discoverCharacteristicsExpectation = + [[XCTestExpectation alloc] initWithDescription:@"Discover characteristics."]; + + [gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + XCTAssertNil(error); + [discoverCharacteristicsExpectation fulfill]; + }]; + + [self waitForExpectations:@[ discoverCharacteristicsExpectation ] timeout:3]; + + XCTAssertEqualObjects(fakePeripheral.services[0].UUID, serviceUUID); + XCTAssertEqualObjects(fakePeripheral.services[0].characteristics[0].UUID, characteristicUUID); +} + +- (void)testDiscoverCharacteristicsWithServiceDiscoveryError { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + fakePeripheral.discoverServicesError = [NSError errorWithDomain:@"fake" code:0 userInfo:nil]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + + XCTestExpectation *discoverCharacteristicsExpectation = + [[XCTestExpectation alloc] initWithDescription:@"Discover characteristics."]; + + // TODO(b/295911088): Failed discovery of services won't trigger the completionHandler until we + // implement a service queue. + discoverCharacteristicsExpectation.inverted = YES; + + [gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + // TODO(b/295911088): Make assertions when service queue is + // implemented. + [discoverCharacteristicsExpectation fulfill]; + }]; + + [self waitForExpectations:@[ discoverCharacteristicsExpectation ] timeout:3]; + + XCTAssertEqual(fakePeripheral.services.count, 0); +} + +- (void)testDiscoverCharacteristicsWithCharacteristicDiscoveryError { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + fakePeripheral.discoverCharacteristicsForServiceError = [NSError errorWithDomain:@"fake" + code:0 + userInfo:nil]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + + XCTestExpectation *discoverCharacteristicsExpectation = + [[XCTestExpectation alloc] initWithDescription:@"Discover characteristics."]; + + [gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + XCTAssertNotNil(error); + [discoverCharacteristicsExpectation fulfill]; + }]; + + [self waitForExpectations:@[ discoverCharacteristicsExpectation ] timeout:3]; + + XCTAssertEqualObjects(fakePeripheral.services[0].UUID, serviceUUID); + XCTAssertEqual(fakePeripheral.services[0].characteristics.count, 0); +} + +// TODO(b/295911088): When service queue is implemented, this is expected to not be an error. +- (void)testDuplicateDiscoverCharacteristics { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + + XCTestExpectation *discoverCharacteristics1Expectation = + [[XCTestExpectation alloc] initWithDescription:@"Discover characteristics 1."]; + XCTestExpectation *discoverCharacteristics2Expectation = + [[XCTestExpectation alloc] initWithDescription:@"Discover characteristics 2."]; + + fakePeripheral.delegateDelay = 1; + [gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + XCTAssertNil(error); + [discoverCharacteristics1Expectation fulfill]; + }]; + + // Queue the delay change so it doesn't immediately overwrite the delay set for the previous + // operation. + dispatch_async(dispatch_get_main_queue(), ^{ + fakePeripheral.delegateDelay = 0; + [gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + XCTAssertNotNil(error); + [discoverCharacteristics2Expectation fulfill]; + }]; + }); + + [self waitForExpectations:@[ + discoverCharacteristics1Expectation, discoverCharacteristics2Expectation + ] + timeout:3]; +} + +- (void)testDiscoverCharacteristicsMultipleCallsWithDifferentServices { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID1 = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *serviceUUID2 = [CBUUID UUIDWithString:kServiceUUID2]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + + XCTestExpectation *discoverCharacteristics1Expectation = + [[XCTestExpectation alloc] initWithDescription:@"Discover characteristics 1."]; + XCTestExpectation *discoverCharacteristics2Expectation = + [[XCTestExpectation alloc] initWithDescription:@"Discover characteristics 2."]; + + fakePeripheral.delegateDelay = 1; + [gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ] + serviceUUID:serviceUUID1 + completionHandler:^(NSError *error) { + XCTAssertNil(error); + [discoverCharacteristics1Expectation fulfill]; + }]; + + // Queue the delay change so it doesn't immediately overwrite the delay set for the previous + // operation. + dispatch_async(dispatch_get_main_queue(), ^{ + fakePeripheral.delegateDelay = 0; + [gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ] + serviceUUID:serviceUUID2 + completionHandler:^(NSError *error) { + XCTAssertNil(error); + [discoverCharacteristics2Expectation fulfill]; + }]; + }); + + [self waitForExpectations:@[ + discoverCharacteristics1Expectation, discoverCharacteristics2Expectation + ] + timeout:3]; + + XCTAssertEqualObjects(fakePeripheral.services[0].UUID, serviceUUID2); + XCTAssertEqualObjects(fakePeripheral.services[0].characteristics[0].UUID, characteristicUUID); + XCTAssertEqualObjects(fakePeripheral.services[1].UUID, serviceUUID1); + XCTAssertEqualObjects(fakePeripheral.services[1].characteristics[0].UUID, characteristicUUID); +} + +- (void)testDiscoverCharacteristicsMultipleCallsWithDifferentCharacteristics { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID1 = [CBUUID UUIDWithString:kCharacteristicUUID1]; + CBUUID *characteristicUUID2 = [CBUUID UUIDWithString:kCharacteristicUUID2]; + + XCTestExpectation *discoverCharacteristics1Expectation = + [[XCTestExpectation alloc] initWithDescription:@"Discover characteristics 1."]; + XCTestExpectation *discoverCharacteristics2Expectation = + [[XCTestExpectation alloc] initWithDescription:@"Discover characteristics 2."]; + + fakePeripheral.delegateDelay = 1; + [gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID1 ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + XCTAssertNil(error); + [discoverCharacteristics1Expectation fulfill]; + }]; + + // Queue the delay change so it doesn't immediately overwrite the delay set for the previous + // operation. + dispatch_async(dispatch_get_main_queue(), ^{ + fakePeripheral.delegateDelay = 0; + [gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID2 ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + XCTAssertNil(error); + [discoverCharacteristics2Expectation fulfill]; + }]; + }); + + [self waitForExpectations:@[ + discoverCharacteristics1Expectation, discoverCharacteristics2Expectation + ] + timeout:3]; + + XCTAssertEqualObjects(fakePeripheral.services[0].UUID, serviceUUID); + XCTAssertEqualObjects(fakePeripheral.services[0].characteristics[0].UUID, characteristicUUID2); + XCTAssertEqualObjects(fakePeripheral.services[0].characteristics[1].UUID, characteristicUUID1); +} + +#pragma mark - Get Characteristic + +- (void)testGetCharacteristic { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + + XCTestExpectation *characteristicExpectation = + [[XCTestExpectation alloc] initWithDescription:@"Get characteristic."]; + + [gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + [gattClient characteristicWithUUID:characteristicUUID + serviceUUID:serviceUUID + completionHandler:^( + GNCBLEGATTCharacteristic *characteristic, + NSError *error) { + XCTAssertNil(error); + XCTAssertNotNil(characteristic); + [characteristicExpectation fulfill]; + }]; + }]; + + [self waitForExpectations:@[ characteristicExpectation ] timeout:3]; +} + +- (void)testGetCharacteristicWithServiceDiscoveryError { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + fakePeripheral.discoverServicesError = [NSError errorWithDomain:@"fake" code:0 userInfo:nil]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + + XCTestExpectation *characteristicExpectation = + [[XCTestExpectation alloc] initWithDescription:@"Get characteristic."]; + + // TODO(b/295911088): Failed discovery of services won't trigger the completionHandler until we + // implement a service queue. + characteristicExpectation.inverted = YES; + + [gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + [gattClient characteristicWithUUID:characteristicUUID + serviceUUID:serviceUUID + completionHandler:^( + GNCBLEGATTCharacteristic *characteristic, + NSError *error) { + // TODO(b/295911088): Make assertions when service + // queue is implemented. + [characteristicExpectation fulfill]; + }]; + }]; + + [self waitForExpectations:@[ characteristicExpectation ] timeout:3]; +} + +- (void)testGetCharacteristicWithCharacteristicDiscoveryError { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + fakePeripheral.discoverCharacteristicsForServiceError = [NSError errorWithDomain:@"fake" + code:0 + userInfo:nil]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + + XCTestExpectation *characteristicExpectation = + [[XCTestExpectation alloc] initWithDescription:@"Get characteristic."]; + + [gattClient discoverCharacteristicsWithUUIDs:@[ characteristicUUID ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + [gattClient characteristicWithUUID:characteristicUUID + serviceUUID:serviceUUID + completionHandler:^( + GNCBLEGATTCharacteristic *characteristic, + NSError *error) { + XCTAssertNotNil(error); + XCTAssertNil(characteristic); + [characteristicExpectation fulfill]; + }]; + }]; + + [self waitForExpectations:@[ characteristicExpectation ] timeout:3]; +} + +- (void)testDuplicateGetCharacteristic { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + + XCTestExpectation *characteristic1Expectation = + [[XCTestExpectation alloc] initWithDescription:@"Get characteristic 1."]; + XCTestExpectation *characteristic2Expectation = + [[XCTestExpectation alloc] initWithDescription:@"Get characteristic 2."]; + + [gattClient + discoverCharacteristicsWithUUIDs:@[ characteristicUUID ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + fakePeripheral.delegateDelay = 1; + [gattClient + characteristicWithUUID:characteristicUUID + serviceUUID:serviceUUID + completionHandler:^(GNCBLEGATTCharacteristic *characteristic, + NSError *error) { + XCTAssertNil(error); + XCTAssertNotNil(characteristic); + [characteristic1Expectation fulfill]; + }]; + // Queue the delay change so it doesn't immediately overwrite the delay set + // for the previous operation. + dispatch_async(dispatch_get_main_queue(), ^{ + fakePeripheral.delegateDelay = 0; + [gattClient + characteristicWithUUID:characteristicUUID + serviceUUID:serviceUUID + completionHandler:^(GNCBLEGATTCharacteristic *characteristic, + NSError *error) { + XCTAssertNil(error); + XCTAssertNotNil(characteristic); + [characteristic2Expectation fulfill]; + }]; + }); + }]; + + [self waitForExpectations:@[ characteristic1Expectation, characteristic2Expectation ] timeout:3]; +} + +- (void)testGetNonExistentCharacteristic { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + + XCTestExpectation *characteristicExpectation = + [[XCTestExpectation alloc] initWithDescription:@"Get characteristic."]; + + [gattClient characteristicWithUUID:characteristicUUID + serviceUUID:serviceUUID + completionHandler:^(GNCBLEGATTCharacteristic *characteristic, NSError *error) { + XCTAssertNotNil(error); + XCTAssertNil(characteristic); + [characteristicExpectation fulfill]; + }]; + + [self waitForExpectations:@[ characteristicExpectation ] timeout:3]; +} + +#pragma mark - Read Value for Characteristic + +- (void)testReadValueForCharacteristic { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + + XCTestExpectation *readValueForCharacteristicExpectation = + [[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic."]; + + [gattClient + discoverCharacteristicsWithUUIDs:@[ characteristicUUID ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + [gattClient + characteristicWithUUID:characteristicUUID + serviceUUID:serviceUUID + completionHandler:^(GNCBLEGATTCharacteristic *characteristic, + NSError *error) { + [gattClient + readValueForCharacteristic:characteristic + completionHandler:^(NSData *value, NSError *error) { + XCTAssertNil(error); + XCTAssertNotNil(value); + [readValueForCharacteristicExpectation fulfill]; + }]; + }]; + }]; + + [self waitForExpectations:@[ readValueForCharacteristicExpectation ] timeout:3]; +} + +- (void)testReadValueForCharacteristicWithServiceDiscoveryError { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + fakePeripheral.discoverServicesError = [NSError errorWithDomain:@"fake" code:0 userInfo:nil]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + + XCTestExpectation *readValueForCharacteristicExpectation = + [[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic."]; + + // TODO(b/295911088): Failed discovery of services won't trigger the completionHandler until we + // implement a service queue. + readValueForCharacteristicExpectation.inverted = YES; + + [gattClient + discoverCharacteristicsWithUUIDs:@[ characteristicUUID ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + [gattClient + characteristicWithUUID:characteristicUUID + serviceUUID:serviceUUID + completionHandler:^(GNCBLEGATTCharacteristic *characteristic, + NSError *error) { + [gattClient + readValueForCharacteristic:characteristic + completionHandler:^(NSData *value, NSError *error) { + // TODO(b/295911088): Make assertions when service + // queue is implemented. + [readValueForCharacteristicExpectation fulfill]; + }]; + }]; + }]; + + [self waitForExpectations:@[ readValueForCharacteristicExpectation ] timeout:3]; +} + +- (void)testReadValueForCharacteristicWithCharacteristicDiscoveryError { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + fakePeripheral.discoverCharacteristicsForServiceError = [NSError errorWithDomain:@"fake" + code:0 + userInfo:nil]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + + XCTestExpectation *readValueForCharacteristicExpectation = + [[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic."]; + + [gattClient + discoverCharacteristicsWithUUIDs:@[ characteristicUUID ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + [gattClient + characteristicWithUUID:characteristicUUID + serviceUUID:serviceUUID + completionHandler:^(GNCBLEGATTCharacteristic *characteristic, + NSError *error) { + [gattClient + readValueForCharacteristic:characteristic + completionHandler:^(NSData *value, NSError *error) { + XCTAssertNotNil(error); + XCTAssertNil(value); + [readValueForCharacteristicExpectation fulfill]; + }]; + }]; + }]; + + [self waitForExpectations:@[ readValueForCharacteristicExpectation ] timeout:3]; +} + +- (void)testReadValueForCharacteristicWithReadValueForCharacteristicError { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + fakePeripheral.readValueForCharacteristicError = [NSError errorWithDomain:@"fake" + code:0 + userInfo:nil]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + + XCTestExpectation *readValueForCharacteristicExpectation = + [[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic."]; + + [gattClient + discoverCharacteristicsWithUUIDs:@[ characteristicUUID ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + [gattClient + characteristicWithUUID:characteristicUUID + serviceUUID:serviceUUID + completionHandler:^(GNCBLEGATTCharacteristic *characteristic, + NSError *error) { + [gattClient + readValueForCharacteristic:characteristic + completionHandler:^(NSData *value, NSError *error) { + XCTAssertNotNil(error); + XCTAssertNil(value); + [readValueForCharacteristicExpectation fulfill]; + }]; + }]; + }]; + + [self waitForExpectations:@[ readValueForCharacteristicExpectation ] timeout:3]; +} + +// TODO(b/295911088): When service queue is implemented, this is expected to not be an error. +- (void)testDuplicateReadValueForCharacteristic { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + + XCTestExpectation *readValueForCharacteristic1Expectation = + [[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic 1."]; + XCTestExpectation *readValueForCharacteristic2Expectation = + [[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic 2."]; + + [gattClient + discoverCharacteristicsWithUUIDs:@[ characteristicUUID ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + [gattClient + characteristicWithUUID:characteristicUUID + serviceUUID:serviceUUID + completionHandler:^(GNCBLEGATTCharacteristic *characteristic, + NSError *error) { + fakePeripheral.delegateDelay = 1; + [gattClient + readValueForCharacteristic:characteristic + completionHandler:^(NSData *value, NSError *error) { + XCTAssertNil(error); + XCTAssertNotNil(value); + [readValueForCharacteristic1Expectation fulfill]; + }]; + + // Queue the delay change so it doesn't immediately overwrite the + // delay set for the previous operation. + dispatch_async(dispatch_get_main_queue(), ^{ + fakePeripheral.delegateDelay = 0; + [gattClient + readValueForCharacteristic:characteristic + completionHandler:^(NSData *value, + NSError *error) { + XCTAssertNotNil(error); + XCTAssertNil(value); + [readValueForCharacteristic2Expectation fulfill]; + }]; + }); + }]; + }]; + + [self waitForExpectations:@[ + readValueForCharacteristic1Expectation, readValueForCharacteristic2Expectation + ] + timeout:3]; +} + +- (void)testReadValueForMultipleCharacteristics { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID1 = [CBUUID UUIDWithString:kCharacteristicUUID1]; + CBUUID *characteristicUUID2 = [CBUUID UUIDWithString:kCharacteristicUUID2]; + + XCTestExpectation *readValueForCharacteristic1Expectation = + [[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic 1."]; + XCTestExpectation *readValueForCharacteristic2Expectation = + [[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic 2."]; + + [gattClient + discoverCharacteristicsWithUUIDs:@[ characteristicUUID1, characteristicUUID2 ] + serviceUUID:serviceUUID + completionHandler:^(NSError *error) { + [gattClient + characteristicWithUUID:characteristicUUID1 + serviceUUID:serviceUUID + completionHandler:^(GNCBLEGATTCharacteristic *characteristic, + NSError *error) { + fakePeripheral.delegateDelay = 1; + [gattClient + readValueForCharacteristic:characteristic + completionHandler:^(NSData *value, NSError *error) { + XCTAssertNil(error); + XCTAssertNotNil(value); + [readValueForCharacteristic1Expectation fulfill]; + }]; + }]; + + [gattClient + characteristicWithUUID:characteristicUUID2 + serviceUUID:serviceUUID + completionHandler:^(GNCBLEGATTCharacteristic *characteristic, + NSError *error) { + // Queue the delay change so it doesn't immediately overwrite the + // delay set for the previous operation. + dispatch_async(dispatch_get_main_queue(), ^{ + fakePeripheral.delegateDelay = 0; + [gattClient + readValueForCharacteristic:characteristic + completionHandler:^(NSData *value, + NSError *error) { + XCTAssertNil(error); + XCTAssertNotNil(value); + [readValueForCharacteristic2Expectation fulfill]; + }]; + }); + }]; + }]; + + [self waitForExpectations:@[ + readValueForCharacteristic1Expectation, readValueForCharacteristic2Expectation + ] + timeout:3]; +} + +- (void)testReadValueForUndiscoveredCharacteristic { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + GNCBLEGATTCharacteristic *characteristic = + [[GNCBLEGATTCharacteristic alloc] initWithUUID:characteristicUUID + serviceUUID:serviceUUID + properties:CBCharacteristicPropertyRead]; + + XCTestExpectation *expectation = + [[XCTestExpectation alloc] initWithDescription:@"Read value for characteristic."]; + + [gattClient readValueForCharacteristic:characteristic + completionHandler:^(NSData *value, NSError *error) { + XCTAssertNotNil(error); + XCTAssertNil(value); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:3]; +} + +#pragma mark - Delegate Calls + +- (void)testUnexpectedDelegateCalls { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + + GNCBLEGATTClient *gattClient = [[GNCBLEGATTClient alloc] initWithPeripheral:fakePeripheral + queue:nil]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1]; + + [fakePeripheral.peripheralDelegate gnc_peripheral:fakePeripheral didDiscoverServices:nil]; + + [fakePeripheral.peripheralDelegate + gnc_peripheral:fakePeripheral + didDiscoverCharacteristicsForService:[[CBMutableService alloc] initWithType:serviceUUID + primary:YES] + error:nil]; + + [fakePeripheral.peripheralDelegate gnc_peripheral:fakePeripheral + didUpdateValueForCharacteristic:[[CBMutableCharacteristic alloc] + initWithType:characteristicUUID + properties:0 + value:nil + permissions:0] + error:nil]; + + // Test to make sure unexpected delegate calls don't cause any issues from missing handlers. + XCTAssertNotNil(gattClient); +} + +@end diff --git a/internal/platform/implementation/apple/Tests/GNCBLEGATTServerTest.m b/internal/platform/implementation/apple/Tests/GNCBLEGATTServerTest.m index 0a3da269..a016a47f 100644 --- a/internal/platform/implementation/apple/Tests/GNCBLEGATTServerTest.m +++ b/internal/platform/implementation/apple/Tests/GNCBLEGATTServerTest.m @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h" +#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h" #import #import #import -#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h" +#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h" #import "internal/platform/implementation/apple/Tests/GNCBLEGATTServer+Testing.h" #import "internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.h" diff --git a/internal/platform/implementation/apple/Tests/GNCFakePeripheral.h b/internal/platform/implementation/apple/Tests/GNCFakePeripheral.h new file mode 100644 index 00000000..0d62478d --- /dev/null +++ b/internal/platform/implementation/apple/Tests/GNCFakePeripheral.h @@ -0,0 +1,60 @@ +// 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/GNCPeripheral.h" + +NS_ASSUME_NONNULL_BEGIN + +/** A fake implementation of @c GNCPeripheral to inject for testing. */ +@interface GNCFakePeripheral : NSObject + +/** + * Similates a @c discoverServices: error. + * + * Setting this error to a value other than @c nil will simulate a failure when calling + * @c discoverServices: and will call the + * @c gnc_peripheral:didDiscoverServices: delegate method with the provided + * error. + */ +@property(nonatomic, nullable, readwrite) NSError *discoverServicesError; + +/** + * Similates a @c discoverCharacteristics:forService: error. + * + * Setting this error to a value other than @c nil will simulate a failure when calling + * @c discoverCharacteristics:forService: and will call the + * @c gnc_peripheral:didDiscoverCharacteristicsForService:error: delegate method with the provided + * error. + */ +@property(nonatomic, nullable, readwrite) NSError *discoverCharacteristicsForServiceError; + +/** + * Similates a @c readValueForCharacteristic: error. + * + * Setting this error to a value other than @c nil will simulate a failure when calling + * @c discoverCharacteristics:forService: and will call the + * @c gnc_peripheral:didUpdateValueForCharacteristic:error: delegate method with the provided + * error. + */ +@property(nonatomic, nullable, readwrite) NSError *readValueForCharacteristicError; + +/** Similates a delay in all delegate calls by the specified amount. */ +@property(nonatomic, readwrite) NSTimeInterval delegateDelay; + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Tests/GNCFakePeripheral.m b/internal/platform/implementation/apple/Tests/GNCFakePeripheral.m new file mode 100644 index 00000000..1fa97973 --- /dev/null +++ b/internal/platform/implementation/apple/Tests/GNCFakePeripheral.m @@ -0,0 +1,118 @@ +// 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/GNCFakePeripheral.h" + +#import +#import +#import + +#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CBService () + +// Change property to readwrite for tests. +@property(retain, readwrite, nullable) NSArray *characteristics; + +@end + +@interface CBCharacteristic () + +// Change property to readwrite for tests. +@property(retain, readwrite, nullable) NSData *value; + +@end + +@implementation GNCFakePeripheral { + NSMutableArray *_services; +} + +@synthesize peripheralDelegate; + +- (instancetype)init { + self = [super init]; + if (self) { + _services = [[NSMutableArray alloc] init]; + } + return self; +} + +- (nullable NSArray *)services { + return _services; +} + +- (void)discoverServices:(nullable NSArray *)serviceUUIDs { + [self delayDelegateUsingBlock:^() { + if (!_discoverServicesError) { + for (CBUUID *serviceUUID in serviceUUIDs) { + [_services addObject:[[CBMutableService alloc] initWithType:serviceUUID primary:YES]]; + } + } + + [peripheralDelegate gnc_peripheral:self didDiscoverServices:_discoverServicesError]; + }]; +} + +- (void)discoverCharacteristics:(nullable NSArray *)characteristicUUIDs + forService:(CBService *)service { + [self delayDelegateUsingBlock:^() { + if (!_discoverCharacteristicsForServiceError) { + NSMutableArray *characteristics = service.characteristics.mutableCopy; + if (!characteristics) { + characteristics = [NSMutableArray array]; + } + for (CBUUID *characteristicUUID in characteristicUUIDs) { + [characteristics addObject:[[CBMutableCharacteristic alloc] + initWithType:characteristicUUID + properties:CBCharacteristicPropertyRead + value:nil + permissions:CBAttributePermissionsReadable]]; + } + service.characteristics = characteristics; + } + + [peripheralDelegate gnc_peripheral:self + didDiscoverCharacteristicsForService:service + error:_discoverCharacteristicsForServiceError]; + }]; +} + +- (void)readValueForCharacteristic:(CBCharacteristic *)characteristic { + [self delayDelegateUsingBlock:^() { + if (!_readValueForCharacteristicError) { + characteristic.value = [NSData data]; + } + + [peripheralDelegate gnc_peripheral:self + didUpdateValueForCharacteristic:characteristic + error:_readValueForCharacteristicError]; + }]; +} + +- (void)delayDelegateUsingBlock:(void (^)())block { + if (_delegateDelay <= 0) { + block(); + } else { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, _delegateDelay * NSEC_PER_SEC), + dispatch_get_main_queue(), ^{ + block(); + }); + } +} + +@end + +NS_ASSUME_NONNULL_END