Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Vibhav Pant
2023-08-27 14:35:07 +05:30
425 changed files with 31448 additions and 7808 deletions
-1
View File
@@ -101,7 +101,6 @@ cc_library(
# TODO: Support WebRTC
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/functional:any_invocable",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings",
+45 -10
View File
@@ -23,9 +23,6 @@ package(default_visibility = [
objc_library(
name = "apple",
srcs = [
"ble.mm",
"ble_utils.mm",
"bluetooth_adapter.mm",
"crypto.mm",
"device_info.mm",
"log_message.mm",
@@ -34,13 +31,9 @@ objc_library(
"preferences_manager.mm",
"scheduled_executor.mm",
"timer.mm",
"utils.mm",
"wifi_lan.mm",
],
hdrs = [
"ble.h",
"ble_utils.h",
"bluetooth_adapter.h",
"device_info.h",
"log_message.h",
"multi_thread_executor.h",
@@ -48,7 +41,6 @@ objc_library(
"scheduled_executor.h",
"single_thread_executor.h",
"timer.h",
"utils.h",
"wifi_lan.h",
],
# Prevent Objective-C++ headers from being pulled into swift.
@@ -57,8 +49,8 @@ objc_library(
deps = [
":Platform_cc",
":Shared",
":ble_v2",
"//internal/platform:base",
"//internal/platform:cancellation_flag",
"//internal/platform/implementation:comm",
"//internal/platform/implementation:platform",
"//internal/platform/implementation:types",
@@ -73,7 +65,6 @@ objc_library(
"@com_google_absl//absl/strings",
"@com_google_absl//absl/synchronization",
"@com_google_absl//absl/time",
"@com_google_absl//absl/types:optional",
"@com_google_absl//absl/types:span",
"@nlohmann_json//:json",
] + select({
@@ -84,6 +75,50 @@ objc_library(
}),
)
objc_library(
name = "ble_v2",
srcs = [
"ble_gatt_client.h",
"ble_gatt_client.mm",
"ble_gatt_server.h",
"ble_gatt_server.mm",
"ble_medium.h",
"ble_medium.mm",
"ble_peripheral.h",
"ble_peripheral.mm",
"ble_server_socket.h",
"ble_server_socket.mm",
"ble_socket.h",
"ble_socket.mm",
"ble_utils.mm",
"bluetooth_adapter_v2.h",
"bluetooth_adapter_v2.mm",
"utils.mm",
],
hdrs = [
"ble.h",
"ble_utils.h",
"utils.h",
],
# Prevent Objective-C++ headers from being pulled into swift.
aspect_hints = ["//tools/build_defs/swift:no_module"],
deps = [
"//internal/platform:base",
"//internal/platform:cancellation_flag",
"//internal/platform:uuid",
"//internal/platform/implementation:comm",
"//internal/platform/implementation/apple/Mediums",
"//internal/platform/implementation/apple/Mediums/Ble/Sockets:Central",
"//internal/platform/implementation/apple/Mediums/Ble/Sockets:Peripheral",
"//third_party/apple_frameworks:CoreBluetooth",
"//third_party/apple_frameworks:Foundation",
"//third_party/objective_c/google_toolbox_for_mac:GTM_Logger",
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/functional:any_invocable",
"@com_google_absl//absl/strings",
],
)
objc_library(
name = "Shared",
srcs = [
@@ -0,0 +1,30 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <Foundation/Foundation.h>
/** The domain for @c NSErrors raised by the BLE medium. */
extern NSErrorDomain const GNCBLEErrorDomain;
/** BLE medium error codes. */
typedef NS_ERROR_ENUM(GNCBLEErrorDomain, GNCBLEError){
GNCBLEErrorUnknown,
GNCBLEErrorDuplicateCharacteristic,
GNCBLEErrorInvalidServiceData,
GNCBLEErrorAlreadyAdvertising,
GNCBLEErrorInvalidCharacteristic,
GNCBLEErrorAlreadyDiscoveringSpecifiedCharacteristics,
GNCBLEErrorAlreadyReadingCharacteristic,
GNCBLEErrorAlreadyScanning,
};
@@ -1,4 +1,4 @@
// Copyright 2022 Google LLC
// 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.
@@ -12,17 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include "internal/platform/implementation/apple/bluetooth_adapter.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEError.h"
#include <string>
#import <Foundation/Foundation.h>
#include "absl/strings/string_view.h"
#include "internal/platform/implementation/bluetooth_adapter.h"
namespace nearby {
namespace apple {
std::string BlePeripheral::GetAddress() const { return adapter_.GetMacAddress(); }
} // namespace apple
} // namespace nearby
NSErrorDomain const GNCBLEErrorDomain = @"com.google.nearby.ble.error";
@@ -15,20 +15,24 @@
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
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
@@ -17,10 +17,17 @@
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
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
@@ -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 <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
@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:(id<GNCPeripheral>)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<CBUUID *> *)characteristicUUIDs
serviceUUID:(CBUUID *)serviceUUID
completionHandler:
(nullable GNCDiscoverCharacteristicsCompletionHandler)completionHandler;
/**
* Retrieves a GATT characteristic.
*
* If you havent 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
@@ -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 <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#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 () <GNCPeripheralDelegate>
@end
@implementation GNCBLEGATTClient {
dispatch_queue_t _queue;
id<GNCPeripheral> _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<CBUUID *, NSMutableDictionary<NSArray<CBUUID *> *,
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<CBUUID *,
NSMutableDictionary<CBUUID *, GNCReadCharacteristicValueCompletionHandler> *>
*_readCharacteristicValueCompletionHandlers;
}
- (instancetype)initWithPeripheral:(id<GNCPeripheral>)peripheral {
return [self
initWithPeripheral:peripheral
queue:dispatch_queue_create(kGNCBLEGATTClientQueueLabel, DISPATCH_QUEUE_SERIAL)];
};
// Private.
- (instancetype)initWithPeripheral:(id<GNCPeripheral>)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<CBUUID *> *)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<GNCPeripheral>)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<NSArray<CBUUID *> *> *groupedCharacteristics =
_discoverCharacteristicsCompletionHandlers[service.UUID].allKeys;
if (!groupedCharacteristics) {
continue;
}
NSMutableSet<CBUUID *> *flattenedCharacteristics = [[NSMutableSet alloc] init];
for (NSArray<CBUUID *> *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<GNCPeripheral>)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<CBUUID *> *characteristics = [[NSMutableSet alloc] init];
for (CBCharacteristic *characteristic in service.characteristics) {
[characteristics addObject:characteristic.UUID];
}
[_discoverCharacteristicsCompletionHandlers[service.UUID].copy
enumerateKeysAndObjectsUsingBlock:^(NSArray<CBUUID *> *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<GNCPeripheral>)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(nullable NSError *)error {
dispatch_assert_queue(_queue);
NSMutableDictionary<CBUUID *, GNCReadCharacteristicValueCompletionHandler> *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
@@ -19,11 +19,43 @@
NS_ASSUME_NONNULL_BEGIN
/**
* A block to be invoked when a call to
* @c createCharacteristicWithServiceID:characteristicUUID:permissions:properties:completionHandler:
* has completed.
*
* @param characteristic The created characteristic, or @c nil if an error occurred.
* @param error The cause of the failure, or @c nil if no error occurred.
*/
typedef void (^GNCCreateCharacteristicCompletionHandler)(
GNCBLEGATTCharacteristic *_Nullable characteristic, NSError *_Nullable error);
/**
* A block to be invoked when a call to @c updateCharacteristic:value:completionHandler: has
* completed.
*
* @param error The cause of the failure, or @c nil if no error occurred.
*/
typedef void (^GNCUpdateCharacteristicCompletionHandler)(NSError *_Nullable error);
/**
* A block to be invoked when a call to @c startAdvertisingData:completionHandler: has completed.
*
* @param error The cause of the failure, or @c nil if no error occurred.
*/
typedef void (^GNCStartAdvertisingCompletionHandler)(NSError *_Nullable error);
/**
* A block to be invoked when a call to @c stopAdvertisingWithcompletionHandler: has completed.
*
* @param error The cause of the failure, or @c nil if no error occurred.
*/
typedef void (^GNCStopAdvertisingCompletionHandler)(NSError *_Nullable error);
/**
* 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.
* @note The public APIs of this class are thread safe.
*/
@interface GNCBLEGATTServer : NSObject
@@ -33,30 +65,31 @@ NS_ASSUME_NONNULL_BEGIN
* 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.
* @param completionHandler Called on a private queue with the characteristic if successfully
* created or an error if one has occured.
*/
- (nullable GNCBLEGATTCharacteristic *)
createCharacteristicWithServiceID:(CBUUID *)serviceUUID
characteristicUUID:(CBUUID *)characteristicUUID
permissions:(CBAttributePermissions)permissions
properties:(CBCharacteristicProperties)properties;
- (void)createCharacteristicWithServiceID:(CBUUID *)serviceUUID
characteristicUUID:(CBUUID *)characteristicUUID
permissions:(CBAttributePermissions)permissions
properties:(CBCharacteristicProperties)properties
completionHandler:
(nullable GNCCreateCharacteristicCompletionHandler)completionHandler;
/**
* 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.
* @param completionHandler Called on a private queue with @c nil if successfully updated or an
* error if one has occured.
*/
- (BOOL)updateCharacteristic:(GNCBLEGATTCharacteristic *)characteristic
value:(nullable NSData *)value;
- (void)updateCharacteristic:(GNCBLEGATTCharacteristic *)characteristic
value:(nullable NSData *)value
completionHandler:(nullable GNCUpdateCharacteristicCompletionHandler)completionHandler;
/** Removes all published services from the local GATT database. */
- (void)stop;
@@ -68,14 +101,23 @@ NS_ASSUME_NONNULL_BEGIN
* 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.
* 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.
* @return Returns @c YES if successfully updated or @c NO if an error has occured.
* @param completionHandler Called on a private queue with @c nil if successfully started
* advertising or an error if one has occured.
*/
- (BOOL)startAdvertisingData:(NSDictionary<CBUUID *, NSData *> *)serviceData;
- (void)startAdvertisingData:(NSDictionary<CBUUID *, NSData *> *)serviceData
completionHandler:(nullable GNCStartAdvertisingCompletionHandler)completionHandler;
/**
* Stops advertising all service data.
*
* @param completionHandler Called on a private queue with @c nil if successfully stopped
* advertising or an error if one has occured.
*/
- (void)stopAdvertisingWithCompletionHandler:
(nullable GNCStopAdvertisingCompletionHandler)completionHandler;
@end
@@ -17,174 +17,193 @@
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#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/GNCPeripheralManager.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/NSData+GNCWebSafeBase64.h"
#import "GoogleToolboxForMac/GTMLogger.h"
NS_ASSUME_NONNULL_BEGIN
// An arbitrary timeout that should be pretty lenient.
static NSTimeInterval const GNCBLEGATTServerTimeoutInSeconds = 2;
static char *const kGNCBLEGATTServerQueueLabel = "com.nearby.GNCBLEGATTServer";
@interface GNCBLEGATTServer () <GNCPeripheralManagerDelegate, CBPeripheralManagerDelegate>
@interface GNCBLEGATTServer () <GNCPeripheralManagerDelegate>
@end
@implementation GNCBLEGATTServer {
dispatch_queue_t _queue;
id<GNCPeripheralManager> _peripheralManager;
// A map of service UUIDs to the service. This is used to keep track of all services and
// characteristics that should be in the GATT database. This also keeps track of a characteristics
// dynamic read value. This is the value returned when a remote device attempts to read the
// characteristic.
// characteristics that have been requested to be added to the GATT database. This may not match
// the "real" services that remote devices can see.
NSMutableDictionary<CBUUID *, CBMutableService *> *_services;
// A list of services that have been requested to be added to the GATT database, but have not yet
// completed. This is used by the create characteristic method to block until the characteristic
// has been added to the database.
NSMutableArray<CBUUID *> *_pendingServiceAdditions;
// A list of characteristics that will be added to the GATT database once the peripheral
// transitions into a valid state. If a characteristic is in this list it means that a
// CoreBluetooth request to add to the characteristic to the GATT database has not yet been made.
NSMutableDictionary<CBUUID *, NSMutableArray<CBMutableCharacteristic *> *>
*_pendingCharacteristics;
// A map of service UUIDs to its associated error if the service had failed to be added to the
// GATT database. This is used by the create characteristic method to determine if the service was
// successfully added.
NSMutableDictionary<CBUUID *, NSError *> *_serviceErrors;
// This keeps track of a characteristic's dynamic read value. This is the value returned when a
// remote device attempts to read the characteristic.
NSMutableDictionary<CBUUID *, NSMutableDictionary<CBUUID *, NSData *> *> *_characteristicValues;
// 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;
// The data that should be advertised. This value is cached until the caller explicitly stops
// advertising, because we will do our best to restart the advertisement if BT turns off then back
// on.
NSDictionary<NSString *, id> *_advertisementData;
}
- (instancetype)init {
dispatch_queue_t queue =
dispatch_queue_create("com.nearby.GNCBLEGATTServer", DISPATCH_QUEUE_SERIAL);
return [self initWithPeripheralManager:[[CBPeripheralManager alloc] initWithDelegate:nil
queue:queue]];
}
- (instancetype)initWithPeripheralManager:(id<GNCPeripheralManager>)peripheralManager {
self = [super init];
if (self) {
_queue = dispatch_queue_create(kGNCBLEGATTServerQueueLabel, DISPATCH_QUEUE_SERIAL);
_peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:nil queue:_queue];
// Set for @c GNCPeripheralManager to be able to forward callbacks.
_peripheralManager.peripheralDelegate = self;
_services = [[NSMutableDictionary alloc] init];
_pendingCharacteristics = [[NSMutableDictionary alloc] init];
_characteristicValues = [[NSMutableDictionary alloc] init];
_advertisementData = nil;
}
return self;
}
// This is private and should only be used for tests. The provided peripheral manager must call
// delegate methods on the main queue.
- (instancetype)initWithPeripheralManager:(nullable id<GNCPeripheralManager>)peripheralManager {
self = [super init];
if (self) {
_queue = dispatch_get_main_queue();
_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];
_pendingCharacteristics = [[NSMutableDictionary alloc] init];
_characteristicValues = [[NSMutableDictionary alloc] init];
_advertisementData = nil;
}
return self;
};
- (nullable GNCBLEGATTCharacteristic *)
createCharacteristicWithServiceID:(CBUUID *)serviceUUID
characteristicUUID:(CBUUID *)characteristicUUID
permissions:(CBAttributePermissions)permissions
properties:(CBCharacteristicProperties)properties {
// Ensure we are in a powered on state.
if (![self waitUntilPoweredOn]) {
return nil;
}
NSDate *timeLimit = [NSDate dateWithTimeIntervalSinceNow:GNCBLEGATTServerTimeoutInSeconds];
BOOL wasSignaled = YES;
[_condition lock];
// If a service with the specified UUID exists, we are modifying it, which requires us to remove
// the service and then re-add it after the new characteristic has been added. Otherwise, create
// the service for the specified UUID and keep track of it.
CBMutableService *service = _services[serviceUUID];
if (service != nil) {
[_peripheralManager removeService:service];
} else {
service = [[CBMutableService alloc] initWithType:serviceUUID primary:YES];
_services[serviceUUID] = service;
}
// Create a characteristic with the specified permissions and properties.
CBMutableCharacteristic *characteristic =
[[CBMutableCharacteristic alloc] initWithType:characteristicUUID
properties:properties
value:nil
permissions:permissions];
// Add the characteristic to the service's list of characteristics
NSMutableArray<CBMutableCharacteristic *> *characteristics =
[service.characteristics mutableCopy];
if (characteristics == nil) {
characteristics = [[NSMutableArray alloc] init];
}
[characteristics addObject:characteristic];
service.characteristics = characteristics;
// Publish the service and wait until complete.
[_pendingServiceAdditions addObject:service.UUID];
[_peripheralManager addService:service];
while ([_pendingServiceAdditions containsObject:service.UUID] && wasSignaled) {
wasSignaled = [_condition waitUntilDate:timeLimit];
}
NSError *serviceError = [_serviceErrors objectForKey:service.UUID];
[_serviceErrors removeObjectForKey:service.UUID];
[_condition unlock];
if (serviceError) {
return nil;
}
return [[GNCBLEGATTCharacteristic alloc] initWithUUID:characteristicUUID
serviceUUID:serviceUUID
permissions:permissions
properties:properties];
}
- (BOOL)updateCharacteristic:(GNCBLEGATTCharacteristic *)characteristic
value:(nullable NSData *)value {
// Ensure we are in a powered on state.
if (![self waitUntilPoweredOn]) {
return NO;
}
[_condition lock];
// Find and update the specified characteristic's value.
CBMutableService *service = _services[characteristic.serviceUUID];
if (!service) {
[_condition unlock];
return NO;
}
for (CBMutableCharacteristic *c in service.characteristics) {
if ([c.UUID isEqual:characteristic.characteristicUUID]) {
c.value = value;
[_condition unlock];
return YES;
- (void)createCharacteristicWithServiceID:(CBUUID *)serviceUUID
characteristicUUID:(CBUUID *)characteristicUUID
permissions:(CBAttributePermissions)permissions
properties:(CBCharacteristicProperties)properties
completionHandler:
(nullable GNCCreateCharacteristicCompletionHandler)completionHandler {
dispatch_async(_queue, ^{
// Check if the characteristic exists in the pending additions or existing services.
for (CBCharacteristic *c in _pendingCharacteristics[serviceUUID]) {
if ([c.UUID isEqual:characteristicUUID]) {
if (completionHandler) {
completionHandler(nil, [NSError errorWithDomain:GNCBLEErrorDomain
code:GNCBLEErrorDuplicateCharacteristic
userInfo:nil]);
}
return;
}
}
}
[_condition unlock];
return NO;
for (CBCharacteristic *c in _services[serviceUUID].characteristics) {
if ([c.UUID isEqual:characteristicUUID]) {
if (completionHandler) {
completionHandler(nil, [NSError errorWithDomain:GNCBLEErrorDomain
code:GNCBLEErrorDuplicateCharacteristic
userInfo:nil]);
}
return;
}
}
// Create a characteristic with the specified permissions and properties.
CBMutableCharacteristic *characteristic =
[[CBMutableCharacteristic alloc] initWithType:characteristicUUID
properties:properties
value:nil
permissions:permissions];
// Track characteristics that need to be added to the GATT database. These will either be added
// immediately or as soon as the peripheral manager is in a valid state.
if (!_pendingCharacteristics[serviceUUID]) {
_pendingCharacteristics[serviceUUID] = [[NSMutableArray alloc] init];
}
[_pendingCharacteristics[serviceUUID] addObject:characteristic];
GNCBLEGATTCharacteristic *gncCharacteristic =
[[GNCBLEGATTCharacteristic alloc] initWithUUID:characteristicUUID
serviceUUID:serviceUUID
permissions:permissions
properties:properties];
[self internalAddPendingServicesIfPoweredOn];
if (completionHandler) {
completionHandler(gncCharacteristic, nil);
}
});
}
- (void)updateCharacteristic:(GNCBLEGATTCharacteristic *)characteristic
value:(nullable NSData *)value
completionHandler:(nullable GNCUpdateCharacteristicCompletionHandler)completionHandler {
dispatch_async(_queue, ^{
// Keep track of the characteristics value, because we will need to retreive it during a read
// request.
if (_characteristicValues[characteristic.serviceUUID] == nil) {
_characteristicValues[characteristic.serviceUUID] = [[NSMutableDictionary alloc] init];
}
_characteristicValues[characteristic.serviceUUID][characteristic.characteristicUUID] = value;
if (completionHandler) {
completionHandler(nil);
}
});
}
- (void)stop {
[_condition lock];
[_services removeAllObjects];
[_condition unlock];
[_peripheralManager removeAllServices];
dispatch_async(_queue, ^{
// Note: Do not clear/stop advertisements here, since there is a separate method for that.
[_peripheralManager removeAllServices];
[_services removeAllObjects];
[_pendingCharacteristics removeAllObjects];
[_characteristicValues removeAllObjects];
});
}
- (BOOL)startAdvertisingData:(NSDictionary<CBUUID *, NSData *> *)serviceData {
// We can only handle advertising a single service data item, so return early if there is more
// than one service incuded.
if (serviceData.count > 1) {
return NO;
}
- (void)startAdvertisingData:(NSDictionary<CBUUID *, NSData *> *)serviceData
completionHandler:(nullable GNCStartAdvertisingCompletionHandler)completionHandler {
dispatch_async(_queue, ^{
// 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) {
if (completionHandler) {
completionHandler([NSError
errorWithDomain:GNCBLEErrorDomain
code:GNCBLEErrorInvalidServiceData
userInfo:@{
NSLocalizedDescriptionKey :
@"Failed to start advertising, because Nearby for Apple platforms only "
@"supports a single service data item."
}]);
}
return;
}
// Ensure we are in a powered on state.
if (![self waitUntilPoweredOn]) {
return NO;
}
// If we have advertisement data set, that means we are already advertising and should return
// early. Advertising must be stopped before being started again.
if (_advertisementData) {
if (completionHandler) {
completionHandler([NSError errorWithDomain:GNCBLEErrorDomain
code:GNCBLEErrorAlreadyAdvertising
userInfo:nil]);
}
return;
}
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];
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.
@@ -192,107 +211,151 @@ static NSTimeInterval const GNCBLEGATTServerTimeoutInSeconds = 2;
encoded = [encoded substringToIndex:22];
}
[_peripheralManager startAdvertising:@{
_advertisementData = @{
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;
[self internalStartAdvertisingIfPoweredOn];
if (completionHandler) {
completionHandler(nil);
}
});
}
#pragma mark - Helpers
- (void)stopAdvertisingWithCompletionHandler:
(nullable GNCStopAdvertisingCompletionHandler)completionHandler {
dispatch_async(_queue, ^{
_advertisementData = nil;
[_peripheralManager stopAdvertising];
if (completionHandler) {
completionHandler(nil);
}
});
}
- (BOOL)waitUntilPoweredOn {
NSDate *timeLimit = [NSDate dateWithTimeIntervalSinceNow:GNCBLEGATTServerTimeoutInSeconds];
BOOL timedOut = NO;
[_condition lock];
while (_peripheralManager.state != CBManagerStatePoweredOn && !timedOut) {
timedOut = ![_condition waitUntilDate:timeLimit];
#pragma mark - Internal
- (void)internalAddPendingServicesIfPoweredOn {
dispatch_assert_queue(_queue);
// Services can only be added while bluetooth is on or off. Since this method will be called
// anytime the peripheral manager's state changes and turning bluetooth on/off does not reset
// the GATT database, we can add all pending services when we transition to the on or off state.
// However, other state transitions may behave differently, so we may need to modify this
// functionality in this future to re-add services if they disapear.
if (_peripheralManager.state == CBManagerStatePoweredOn ||
_peripheralManager.state == CBManagerStatePoweredOff) {
for (CBUUID *serviceUUID in _pendingCharacteristics.allKeys) {
// 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;
}
// Add the pending characteristics to the current service's list of characteristics.
NSMutableArray<CBMutableCharacteristic *> *characteristics =
[service.characteristics mutableCopy];
if (characteristics == nil) {
characteristics = [[NSMutableArray alloc] init];
}
// This makes the assumption that _pendingCharacteristics does not contain any duplicate
// characteristics within itself or the current service characteristics. This is only
// enforced on characteristic creation.
[characteristics addObjectsFromArray:_pendingCharacteristics[serviceUUID]];
service.characteristics = characteristics;
// Publish the service.
[_peripheralManager addService:service];
}
// Clean up pending characteristics so we don't try to re-add them any time the power state
// changes.
[_pendingCharacteristics removeAllObjects];
}
}
- (void)internalStartAdvertisingIfPoweredOn {
dispatch_assert_queue(_queue);
// Advertising can only be done when powered on and must be restarted if bluetooth is turned off
// then back on. This will be called anytime the peripheral manager's state changes, so
// @c startAdvertising: will be called anytime state transitions back to powered on.
if (_peripheralManager.state == CBManagerStatePoweredOn && _advertisementData != nil) {
// Stop advertising just in case something outside of this class is advertising (like weave).
[_peripheralManager stopAdvertising];
[_peripheralManager startAdvertising:_advertisementData];
}
[_condition unlock];
return _peripheralManager.state == CBManagerStatePoweredOn;
}
#pragma mark - GNCPeripheralManagerDelegate
- (void)gnc_peripheralManagerDidUpdateState:(id<GNCPeripheralManager>)peripheral {
[_condition lock];
[_condition signal];
[_condition unlock];
dispatch_assert_queue(_queue);
[self internalAddPendingServicesIfPoweredOn];
[self internalStartAdvertisingIfPoweredOn];
}
- (void)gnc_peripheralManagerDidStartAdvertising:(id<GNCPeripheralManager>)peripheral
error:(nullable NSError *)error {
[_condition lock];
[_condition signal];
[_condition unlock];
dispatch_assert_queue(_queue);
if (error) {
GTMLoggerError(@"Failed to start advertising: %@", error);
}
}
- (void)gnc_peripheralManager:(id<GNCPeripheralManager>)peripheral
didAddService:(CBService *)service
error:(nullable NSError *)error {
[_condition lock];
[_pendingServiceAdditions removeObject:service.UUID];
dispatch_assert_queue(_queue);
if (error) {
[_serviceErrors setObject:error forKey:service.UUID];
GTMLoggerError(@"Failed to add service %@: %@", service, error);
}
[_condition signal];
[_condition unlock];
}
- (void)gnc_peripheralManager:(id<GNCPeripheralManager>)peripheral
didReceiveReadRequest:(CBATTRequest *)request {
[_condition lock];
// Find the requested charactertic and respond with its value if it exists.
CBMutableService *service = _services[request.characteristic.service.UUID];
if (!service) {
[_condition unlock];
dispatch_assert_queue(_queue);
NSData *value =
_characteristicValues[request.characteristic.service.UUID][request.characteristic.UUID];
if (!value) {
[_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];
request.value = value;
[_peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
}
#pragma mark - CBPeripheralManagerDelegate
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
[self gnc_peripheralManagerDidUpdateState:peripheral];
dispatch_async(_queue, ^{
[self gnc_peripheralManagerDidUpdateState:peripheral];
});
}
- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral
error:(nullable NSError *)error {
[self gnc_peripheralManagerDidStartAdvertising:peripheral error:error];
dispatch_async(_queue, ^{
[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];
dispatch_async(_queue, ^{
[self gnc_peripheralManager:peripheral didAddService:service error:error];
});
}
- (void)peripheralManager:(CBPeripheralManager *)peripheral
didReceiveReadRequest:(CBATTRequest *)request {
[self gnc_peripheralManager:peripheral didReceiveReadRequest:request];
dispatch_async(_queue, ^{
[self gnc_peripheralManager:peripheral didReceiveReadRequest:request];
});
}
@end
@@ -0,0 +1,171 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
@class GNCBLEGATTServer;
@class GNCBLEGATTClient;
@class GNCBLEGATTCharacteristic;
@protocol GNCPeripheral;
NS_ASSUME_NONNULL_BEGIN
/**
* A block to be invoked when a call to @c startAdvertisingData:completionHandler: has completed.
*
* @param error The cause of the failure, or @c nil if no error occurred.
*/
typedef void (^GNCStartAdvertisingCompletionHandler)(NSError *_Nullable error);
/**
* A block to be invoked when a call to @c stopAdvertisingWithcompletionHandler: has completed.
*
* @param error The cause of the failure, or @c nil if no error occurred.
*/
typedef void (^GNCStopAdvertisingCompletionHandler)(NSError *_Nullable error);
/**
* A block to be invoked when a peripherals advertisement has been discovered.
*
* @note This block can be called numerous times.
*
* @param peripheral The discovered peripheral.
* @param serviceData A dictionary that contains service-specific advertisement data. The keys
* represent services and the values represent the service-specific data.
*/
typedef void (^GNCAdvertisementFoundHandler)(id<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *serviceData);
/**
* A block to be invoked when a call to
* @c startScanningForService:advertisementFoundHandler:completionHandler: has completed.
*
* @param error The cause of the failure, or @c nil if no error occurred.
*/
typedef void (^GNCStartScanningCompletionHandler)(NSError *_Nullable error);
/**
* A block to be invoked when a call to @c stopScanningWithCompletionHandler: has completed.
*
* @param error The cause of the failure, or @c nil if no error occurred.
*/
typedef void (^GNCStopScanningCompletionHandler)(NSError *_Nullable error);
/**
* A block to be invoked when a call to @c startGATTServerWithCompletionHandler: has completed.
*
* @param server The successfully started GATT server, or @c nil if an error occurred.
* @param error The cause of the failure, or @c nil if no error occurred.
*/
typedef void (^GNCGATTServerCompletionHandler)(GNCBLEGATTServer *_Nullable server,
NSError *_Nullable error);
/** A block to be invoked when a peripheral has disconnected. */
typedef void (^GNCGATTDisconnectionHandler)();
/**
* A block to be invoked when a call to
* @c connectToGATTServerForPeripheral:disconnectionHandler:completionHandler: has completed.
*
* @param client The interface to the remote peripherals GATT server, or @c nil if an error
* occurred.
* @param error The cause of the failure, or @c nil if no error occurred.
*/
typedef void (^GNCGATTConnectionCompletionHandler)(GNCBLEGATTClient *_Nullable client,
NSError *_Nullable error);
/**
* The main BLE medium used inside of Nearby. This serves as the entry point for all BLE and GATT
* related operations.
*
* @note The public APIs of this class are thread safe.
*/
@interface GNCBLEMedium : NSObject
/** The hardware supports BOTH advertising extensions and extended scans. */
@property(nonatomic, readonly) BOOL supportsExtendedAdvertisements;
/**
* Starts advertising service data in a way that is supported by CoreBluetooth.
*
* Since CoreBluetooth doesn't support setting the @c CBAdvertisementDataServiceDataKey key, the
* service list is advertised using @c CBAdvertisementDataServiceUUIDsKey and the associated data is
* advertised using @c CBAdvertisementDataLocalNameKey. Since @c CBAdvertisementDataLocalNameKey
* does not support binary data, the value is base64 encoded and truncated if the resulting value is
* longer than 22 bytes. This also means we can only support advertising a single service.
*
* @param serviceData A dictionary that contains service-specific advertisement data.
* @param completionHandler Called on a private queue with @c nil if successfully started
* advertising or an error if one has occured.
*/
- (void)startAdvertisingData:(NSDictionary<CBUUID *, NSData *> *)serviceData
completionHandler:(nullable GNCStartAdvertisingCompletionHandler)completionHandler;
/**
* Stops advertising all service data.
*
* @param completionHandler Called on a private queue with @c nil if successfully stopped
* advertising or an error if one has occured.
*/
- (void)stopAdvertisingWithCompletionHandler:
(nullable GNCStopAdvertisingCompletionHandler)completionHandler;
/**
* Scans for peripherals that are advertising the specified service.
*
* @param serviceUUID The service UUID to scan for.
* @param advertisementFoundHandler Called on a private queue when a peripheral has been discovered.
* @param completionHandler Called on a private queue with @c nil if successfully started scanning
* or an error if one has occured.
*/
- (void)startScanningForService:(CBUUID *)serviceUUID
advertisementFoundHandler:(GNCAdvertisementFoundHandler)advertisementFoundHandler
completionHandler:(nullable GNCStartScanningCompletionHandler)completionHandler;
/**
* Stops scanning for peripherals.
*
* @param completionHandler Called on a private queue with @c nil if successfully stopped
* scanning or an error if one has occured.
*/
- (void)stopScanningWithCompletionHandler:
(nullable GNCStopScanningCompletionHandler)completionHandler;
/**
* Starts a GATT server.
*
* @param completionHandler Called on a private queue with the GATT server if successfully started
* or an error if one has occured.
*/
- (void)startGATTServerWithCompletionHandler:
(nullable GNCGATTServerCompletionHandler)completionHandler;
/**
* Connects to a peripherals GATT server.
*
* @param remotePeripheral The peripheral to which the central is attempting to connect.
* @param disconnectionHandler Called on a private queue when the peripheral has been disconnected.
* @param completionHandler Called on a private queue with a GATT client if successfully connected
* or an error if one has occured.
*/
- (void)connectToGATTServerForPeripheral:(id<GNCPeripheral>)remotePeripheral
disconnectionHandler:(nullable GNCGATTDisconnectionHandler)disconnectionHandler
completionHandler:
(nullable GNCGATTConnectionCompletionHandler)completionHandler;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,319 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEMedium.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEError.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTClient.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCCentralManager.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/NSData+GNCWebSafeBase64.h"
NS_ASSUME_NONNULL_BEGIN
static char *const kBLEMediumQueueLabel = "com.nearby.GNCBLEMedium";
static NSError *AlreadyScanningError() {
return [NSError errorWithDomain:GNCBLEErrorDomain code:GNCBLEErrorAlreadyScanning userInfo:nil];
}
@interface GNCBLEMedium () <GNCCentralManagerDelegate, CBCentralManagerDelegate>
@end
@implementation GNCBLEMedium {
dispatch_queue_t _queue;
id<GNCCentralManager> _centralManager;
// The active GATT server, or @nil if one hasn't been started yet.
GNCBLEGATTServer *_server;
// The service that is being actively scanned for, or @c nil if not currently scanning.
CBUUID *_serviceUUID;
// The handler called when an advertisement for the service represented by @c _serviceUUID has
// been discovered. This will be called continuously, until the peripheral disappears.
GNCAdvertisementFoundHandler _advertisementFoundHandler;
// A peripheral to connection completion handler map. Used to track connection attempts. When a
// connection attempt has succeeded or failed, the completion handler is called and removed from
// the map.
NSMutableDictionary<NSUUID *, GNCGATTConnectionCompletionHandler> *_connectionCompletionHandlers;
// A peripheral to disconnection handler map. Used to track when a peripheral becomes
// disconnected. Once disconnected, the completion handler is called and removed from the map.
NSMutableDictionary<NSUUID *, GNCGATTDisconnectionHandler> *_disconnectionHandlers;
}
- (instancetype)init {
CBCentralManager *centralManager =
[[CBCentralManager alloc] initWithDelegate:self
queue:_queue
options:@{CBCentralManagerOptionShowPowerAlertKey : @NO}];
dispatch_queue_t queue = dispatch_queue_create(kBLEMediumQueueLabel, DISPATCH_QUEUE_SERIAL);
return [self initWithCentralManager:centralManager queue:queue];
}
// This is private and should only be used for tests. The provided central manager must call
// delegate methods on the main queue.
- (instancetype)initWithCentralManager:(id<GNCCentralManager>)centralManager
queue:(nullable dispatch_queue_t)queue {
self = [super init];
if (self) {
_queue = queue ?: dispatch_get_main_queue();
_centralManager = centralManager;
_centralManager.centralDelegate = self;
_connectionCompletionHandlers = [NSMutableDictionary dictionary];
_disconnectionHandlers = [NSMutableDictionary dictionary];
}
return self;
}
- (BOOL)supportsExtendedAdvertisements {
// TODO(b/294736083): CoreBluetooth doesn't support actually advertising any extensions, however
// some devices can scan for them if the feature is available. If we return @c YES from this
// method, we would be enabling advertising extensions (which won't work), so we must return @c NO
// until we add support for a new method to check only if extended scans are supported.
return NO;
}
- (void)startAdvertisingData:(NSDictionary<CBUUID *, NSData *> *)serviceData
completionHandler:(nullable GNCStartAdvertisingCompletionHandler)completionHandler {
dispatch_async(_queue, ^{
if (!_server) {
_server = [[GNCBLEGATTServer alloc] init];
}
[_server startAdvertisingData:serviceData completionHandler:completionHandler];
});
}
- (void)stopAdvertisingWithCompletionHandler:
(nullable GNCStopAdvertisingCompletionHandler)completionHandler {
dispatch_async(_queue, ^{
if (!_server) {
if (completionHandler) {
completionHandler(nil);
}
return;
}
[_server stopAdvertisingWithCompletionHandler:completionHandler];
});
}
- (void)startScanningForService:(CBUUID *)serviceUUID
advertisementFoundHandler:(GNCAdvertisementFoundHandler)advertisementFoundHandler
completionHandler:(nullable GNCStartScanningCompletionHandler)completionHandler {
dispatch_async(_queue, ^{
if (_serviceUUID) {
if (completionHandler) {
completionHandler(AlreadyScanningError());
}
return;
}
_serviceUUID = serviceUUID;
_advertisementFoundHandler = advertisementFoundHandler;
[self internalStartScanningIfPoweredOn];
if (completionHandler) {
completionHandler(nil);
}
});
}
- (void)stopScanningWithCompletionHandler:
(nullable GNCStopScanningCompletionHandler)completionHandler {
dispatch_async(_queue, ^{
_serviceUUID = nil;
_advertisementFoundHandler = nil;
[_centralManager stopScan];
if (completionHandler) {
completionHandler(nil);
}
});
}
- (void)startGATTServerWithCompletionHandler:
(nullable GNCGATTServerCompletionHandler)completionHandler {
dispatch_async(_queue, ^{
if (!_server) {
_server = [[GNCBLEGATTServer alloc] init];
}
if (completionHandler) {
completionHandler(_server, nil);
}
});
}
- (void)connectToGATTServerForPeripheral:(id<GNCPeripheral>)remotePeripheral
disconnectionHandler:(nullable GNCGATTDisconnectionHandler)disconnectionHandler
completionHandler:
(nullable GNCGATTConnectionCompletionHandler)completionHandler {
dispatch_async(_queue, ^{
_disconnectionHandlers[remotePeripheral.identifier] = disconnectionHandler;
_connectionCompletionHandlers[remotePeripheral.identifier] = completionHandler;
[_centralManager connectPeripheral:remotePeripheral options:@{}];
});
}
#pragma mark - Internal
- (void)internalStartScanningIfPoweredOn {
dispatch_assert_queue(_queue);
// Scanning can only be done when powered on and must be restarted if bluetooth is turned off
// then back on. This will be called anytime the central manager's state changes, so
// @c scanForPeripheralsWithServices:options: will be called anytime state transitions back to
// powered on.
if (_centralManager.state == CBManagerStatePoweredOn && _serviceUUID != nil) {
// Stop scanning just in case something outside of this class is already scanning.
[_centralManager stopScan];
[_centralManager
scanForPeripheralsWithServices:@[ _serviceUUID ]
// Nearby relies on the existence of an advertisement for endpoint
// discovery/lost events, so we must set this key to keep the stream
// of duplicate delegate events flowing. This has adverse effect on
// battery life, but currently necessary.
options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES}];
}
}
- (NSDictionary<CBUUID *, NSData *> *)decodeAdvertisementData:
(NSDictionary<NSString *, id> *)advertisementData {
dispatch_assert_queue(_queue);
// If service data is available, return it directly.
NSDictionary<CBUUID *, NSData *> *serviceData =
advertisementData[CBAdvertisementDataServiceDataKey];
if (serviceData) {
return serviceData;
}
// Apple devices don't support advertising service data, so Apple devices advertise a base64
// encoded local name, while other devices advertise service data. Here we attempt to reconstruct
// service data by decoding the local name. If successful, this is possibly a Nearby advertisement
// on an Apple device.
NSString *localName = advertisementData[CBAdvertisementDataLocalNameKey];
if (!localName) {
return @{};
}
NSData *data = [[NSData alloc] initWithWebSafeBase64EncodedString:localName];
// A Nearby Apple advertisement should only have a single service, so simply grab the first one if
// it exists.
NSArray<CBUUID *> *serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey];
CBUUID *serviceUUID = serviceUUIDs.firstObject;
if (data && serviceUUID) {
return @{serviceUUID : data};
}
return @{};
}
#pragma mark - GNCCentralManagerDelegate
- (void)gnc_centralManagerDidUpdateState:(id<GNCCentralManager>)central {
dispatch_assert_queue(_queue);
[self internalStartScanningIfPoweredOn];
}
- (void)gnc_centralManager:(id<GNCCentralManager>)central
didDiscoverPeripheral:(id<GNCPeripheral>)peripheral
advertisementData:(NSDictionary<NSString *, id> *)advertisementData
RSSI:(NSNumber *)RSSI {
dispatch_assert_queue(_queue);
if (_advertisementFoundHandler) {
_advertisementFoundHandler(peripheral, [self decodeAdvertisementData:advertisementData]);
}
}
- (void)gnc_centralManager:(id<GNCCentralManager>)central
didConnectPeripheral:(id<GNCPeripheral>)peripheral {
dispatch_assert_queue(_queue);
GNCGATTConnectionCompletionHandler handler = _connectionCompletionHandlers[peripheral.identifier];
_connectionCompletionHandlers[peripheral.identifier] = nil;
if (handler) {
GNCBLEGATTClient *client = [[GNCBLEGATTClient alloc] initWithPeripheral:peripheral];
handler(client, nil);
}
}
- (void)gnc_centralManager:(id<GNCCentralManager>)central
didFailToConnectPeripheral:(id<GNCPeripheral>)peripheral
error:(nullable NSError *)error {
dispatch_assert_queue(_queue);
GNCGATTConnectionCompletionHandler handler = _connectionCompletionHandlers[peripheral.identifier];
_connectionCompletionHandlers[peripheral.identifier] = nil;
if (handler) {
handler(nil, error);
}
}
- (void)gnc_centralManager:(id<GNCCentralManager>)central
didDisconnectPeripheral:(id<GNCPeripheral>)peripheral
error:(nullable NSError *)error {
dispatch_assert_queue(_queue);
GNCGATTDisconnectionHandler handler = _disconnectionHandlers[peripheral.identifier];
_disconnectionHandlers[peripheral.identifier] = nil;
if (handler) {
handler();
}
}
#pragma mark - CBCentralManagerDelegate
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
dispatch_async(_queue, ^{
[self gnc_centralManagerDidUpdateState:central];
});
}
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary<NSString *, id> *)advertisementData
RSSI:(NSNumber *)RSSI {
dispatch_async(_queue, ^{
[self gnc_centralManager:central
didDiscoverPeripheral:peripheral
advertisementData:advertisementData
RSSI:RSSI];
});
}
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
dispatch_async(_queue, ^{
[self gnc_centralManager:central didConnectPeripheral:peripheral];
});
}
- (void)centralManager:(CBCentralManager *)central
didFailToConnectPeripheral:(CBPeripheral *)peripheral
error:(nullable NSError *)error {
dispatch_async(_queue, ^{
[self gnc_centralManager:central didFailToConnectPeripheral:peripheral error:error];
});
}
- (void)centralManager:(CBCentralManager *)central
didDisconnectPeripheral:(CBPeripheral *)peripheral
error:(nullable NSError *)error {
dispatch_async(_queue, ^{
[self gnc_centralManager:central didDisconnectPeripheral:peripheral error:error];
});
}
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,181 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
@protocol GNCCentralManagerDelegate;
@protocol GNCPeripheral;
NS_ASSUME_NONNULL_BEGIN
/** Protocol which helps create a fake of a @c CBCentralManager to inject for testing. */
@protocol GNCCentralManager
/** Shadow property of a @c CBCentralManagerDelegate. */
@property(weak, nonatomic, nullable) id<GNCCentralManagerDelegate> centralDelegate;
/**
* The current state of the manager.
*
* This state is initially set to @c CBManagerStateUnknown. When the state updates, the manager
* calls its delegates @c gnc_centralManagerDidUpdateState: method.
*/
@property(nonatomic, assign, readonly) CBManagerState state;
/**
* Scans for peripherals that are advertising services.
*
* You can provide an array of @c CBUUID objects, representing service UUIDs, in the @c serviceUUIDs
* parameter. When you do, the central manager returns only peripherals that advertise the services
* you specify. If the @c serviceUUIDs parameter is @c nil, this method returns all discovered
* peripherals, regardless of their supported services.
*
* @note The recommended practice is to populate the @c serviceUUIDs parameter rather than leaving
* it @c nil.
*
* If the central manager is actively scanning with one set of parameters and it receives another
* set to scan, the new parameters override the previous set. When the central manager discovers a
* peripheral, it calls the @c gnc_centralManager:didDiscoverPeripheral:advertisementData:RSSI:
* method of its delegate object.
*
* Your app can scan for Bluetooth devices in the background by specifying the @c bluetooth-central
* background mode. To do this, your app must explicitly scan for one or more services by specifying
* them in the @c serviceUUIDs parameter. The CBCentralManager scan option has no effect while
* scanning in the background.
*
* @param serviceUUIDs An array of @c CBUUID objects that the app is interested in. Each @c CBUUID
* object represents the UUID of a service that a peripheral advertises.
* @param options A dictionary of options for customizing the scan.
*/
- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs
options:(nullable NSDictionary<NSString *, id> *)options;
/**
* Establishes a local connection to a peripheral.
*
* After successfully establishing a local connection to a peripheral, the central manager object
* calls the @c gnc_centralManager:didConnectPeripheral: method of its delegate object. If the
* connection attempt fails, the central manager object calls the
* @c gnc_centralManager:didFailToConnectPeripheral:error: method of its delegate object instead.
* Attempts to connect to a peripheral dont time out. To explicitly cancel a pending connection to
* a peripheral, call the @c cancelPeripheralConnection: method. Deallocating @c peripheral also
* implicitly calls @c cancelPeripheralConnection:.
*
* @param peripheral The peripheral to which the central is attempting to connect.
* @param options A dictionary to customize the behavior of the connection.
*/
- (void)connectPeripheral:(id<GNCPeripheral>)peripheral
options:(nullable NSDictionary<NSString *, id> *)options;
/** Asks the central manager to stop scanning for peripherals. */
- (void)stopScan;
@end
/**
* Protocol which helps the @c GNCCentralManager wrap a @c CBCentralManagerDelegate for
* testing.
*/
@protocol GNCCentralManagerDelegate <NSObject>
/**
* Tells the delegate the central managers state updated.
*
* You implement this required method to ensure that the central device supports Bluetooth low
* energy and that its available to use. You should issue commands to the central manager only when
* the central managers @c state indicates its powered on. A state with a value lower than
* @c CBManagerStatePoweredOn implies that scanning has stopped, which in turn disconnects any
* previously-connected peripherals. If the state moves below @c CBManagerStatePoweredOff, all
* @c CBPeripheral objects obtained from this central manager become invalid; you must retrieve or
* discover these peripherals again.
*
* @param central The central manager whose state has changed.
*/
- (void)gnc_centralManagerDidUpdateState:(id<GNCCentralManager>)central;
/**
* Tells the delegate the central manager discovered a peripheral while scanning for devices.
*
* You must retain a local copy of the peripheral if you want to perform commands on it. Use the
* RSSI data to determine the proximity of a discoverable peripheral device, and whether you want to
* connect to it automatically.
*
* @param central The central manager that provides the update.
* @param peripheral The discovered peripheral.
* @param advertisementData A dictionary containing any advertisement data.
* @param RSSI The current received signal strength indicator (RSSI) of the peripheral, in decibels.
*/
- (void)gnc_centralManager:(id<GNCCentralManager>)central
didDiscoverPeripheral:(id<GNCPeripheral>)peripheral
advertisementData:(NSDictionary<NSString *, id> *)advertisementData
RSSI:(NSNumber *)RSSI;
/**
* Tells the delegate that the central manager connected to a peripheral.
*
* The manager invokes this method when a call to @c connectPeripheral:options: succeeds. You
* typically implement this method to set the peripherals delegate and discover its services.
*
* @param central The central manager that provides this information.
* @param peripheral The now-connected peripheral.
*/
- (void)gnc_centralManager:(id<GNCCentralManager>)central
didConnectPeripheral:(id<GNCPeripheral>)peripheral;
/**
* Tells the delegate the central manager failed to create a connection with a peripheral.
*
* The manager invokes this method when a connection initiated with the
* @c connectPeripheral:options: method fails to complete. Because connection attempts dont time
* out, a failed connection usually indicates a transient issue, in which case you may attempt
* connecting to the peripheral again.
*
* @param central The central manager that provides this information.
* @param peripheral The peripheral that failed to connect.
* @param error The cause of the failure, or @c nil if no error occurred.
*/
- (void)gnc_centralManager:(id<GNCCentralManager>)central
didFailToConnectPeripheral:(id<GNCPeripheral>)peripheral
error:(nullable NSError *)error;
/**
* Tells the delegate that the central manager disconnected from a peripheral.
*
* The manager invokes this method when disconnecting a peripheral previously connected with the
* @c connectPeripheral:options: method. The error parameter contains the reason for the
* disconnection, unless the disconnect resulted from a call to @c cancelPeripheralConnection:.
*
* All services, characteristics, and characteristic descriptors of a peripheral become invalidated
* after it disconnects.
*
* @param central The central manager that provides this information.
* @param peripheral The now-disconnected peripheral.
* @param error The cause of the failure, or @c nil if no error occurred.
*/
- (void)gnc_centralManager:(id<GNCCentralManager>)central
didDisconnectPeripheral:(id<GNCPeripheral>)peripheral
error:(nullable NSError *)error;
@end
/**
* Declares that @c CBCentralManager implements the @c GNCCentralManager protocol.
*
* This allows us to directly use a @c CBCentralManager as a @c GNCCentralManager.
*/
@interface CBCentralManager () <GNCCentralManager>
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,36 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCCentralManager.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@implementation CBCentralManager (GNCCentralManagerAdditions)
- (void)setCentralDelegate:(nullable id<GNCCentralManagerDelegate>)centralDelegate {
NSAssert([centralDelegate conformsToProtocol:@protocol(CBCentralManagerDelegate)],
@"centralDelegate must conform to protocol CBCentralManagerDelegate");
self.delegate = (id<CBCentralManagerDelegate>)centralDelegate;
}
- (nullable id<GNCCentralManagerDelegate>)centralDelegate {
return (id<GNCCentralManagerDelegate>)self.delegate;
}
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,177 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
@protocol 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<GNCPeripheralDelegate> peripheralDelegate;
/**
* A list of a peripherals discovered services.
*
* Returns an array of services (represented by CBService objects) that successful a call to the
* @c discoverServices: method discovered. If you havent 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<CBService *> *services;
/**
* The UUID associated with the peer.
*
* The value of this property represents the unique identifier of the peer. The first time a local
* manager encounters a peer, the system assigns the peer a UUID, represented by a new @c NSUUID
* object. Peers use @c NSUUID instances to identify themselves, instead of by the @c CBUUID objects
* that identify a peripherals services, characteristics, and descriptors.
*/
@property(readonly, nonatomic) NSUUID *identifier;
/**
* Discovers the specified services of the peripheral.
*
* 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 peripherals
* 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 peripherals @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<CBUUID *> *)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 services
* 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 services characteristics, you can access them through the services
* @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<CBUUID *> *)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
* characteristics @c value property.
*
* Not all characteristics have a readable value. You can determine whether a characteristics 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 <CBPeripheralDelegate>
/**
* 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 peripherals @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<GNCPeripheral>)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 services @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<GNCPeripheral>)peripheral
didDiscoverCharacteristicsForService:(CBService *)service
error:(nullable NSError *)error;
/**
* Tells the delegate that retrieving the specified characteristics value succeeded, or that the
* characteristics 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<GNCPeripheral>)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 () <GNCPeripheral>
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,34 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@implementation CBPeripheral (GNCPeripheralAdditions)
- (void)setPeripheralDelegate:(nullable id<GNCPeripheralDelegate>)peripheralDelegate {
self.delegate = peripheralDelegate;
}
- (nullable id<GNCPeripheralDelegate>)peripheralDelegate {
return (id<GNCPeripheralDelegate>)self.delegate;
}
@end
NS_ASSUME_NONNULL_END
@@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN
@protocol GNCPeripheralManager
/** Shadow property of a @c CBPeripheralManagerDelegate. */
@property(nonatomic, nullable) id<GNCPeripheralManagerDelegate> peripheralDelegate;
@property(weak, nonatomic, nullable) id<GNCPeripheralManagerDelegate> peripheralDelegate;
@property(nonatomic, assign, readonly) CBManagerState state;
@@ -111,13 +111,20 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)respondToRequest:(CBATTRequest *)request withResult:(CBATTError)result;
/**
* Stops advertising peripheral manager data.
*
* Call this method when you no longer want to advertise peripheral manager data.
*/
- (void)stopAdvertising;
@end
/**
* Protocol which helps the @c GNCPeripheralManager wrap a @c CBPeripheralManagerDelegate for
* testing.
*/
@protocol GNCPeripheralManagerDelegate <NSObject>
@protocol GNCPeripheralManagerDelegate <CBPeripheralManagerDelegate>
/**
* Tells the delegate the peripheral managers state updated.
@@ -23,9 +23,7 @@ NS_ASSUME_NONNULL_BEGIN
@implementation CBPeripheralManager (GNCPeripheralManagerAdditions)
- (void)setPeripheralDelegate:(nullable id<GNCPeripheralManagerDelegate>)peripheralDelegate {
NSAssert([peripheralDelegate conformsToProtocol:@protocol(CBPeripheralManagerDelegate)],
@"peripheralDelegate must conform to protocol CBPeripheralManagerDelegate");
self.delegate = (id<CBPeripheralManagerDelegate>)peripheralDelegate;
self.delegate = peripheralDelegate;
}
- (nullable id<GNCPeripheralManagerDelegate>)peripheralDelegate {
@@ -19,7 +19,16 @@ NS_ASSUME_NONNULL_BEGIN
@interface NSData (GNCWebSafeBase64)
/** Creates a Base64 encoded string from the data using websafe characters and no padding. */
- (NSString *)webSafebase64EncodedString;
- (NSString *)webSafeBase64EncodedString;
/**
* Initializes a data object with the given Base64 encoded string.
*
* @param base64String A Base64 encoded string.
* @return A data object built by Base64 decoding the provided string. Returns @c nil if the data
* object could not be decoded.
*/
- (nullable instancetype)initWithWebSafeBase64EncodedString:(NSString *)base64String;
@end
@@ -14,11 +14,13 @@
#import "internal/platform/implementation/apple/Mediums/BLEv2/NSData+GNCWebSafeBase64.h"
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@implementation NSData (GNCWebSafeBase64)
- (NSString *)webSafebase64EncodedString {
- (NSString *)webSafeBase64EncodedString {
NSString *encoded = [self base64EncodedStringWithOptions:0];
// Convert the standard base64 characters to URL safe variants.
@@ -29,6 +31,20 @@ NS_ASSUME_NONNULL_BEGIN
return encoded;
}
- (nullable instancetype)initWithWebSafeBase64EncodedString:(NSString *)base64String {
// Convert the URL safe base64 characters to the standard variants.
base64String = [base64String stringByReplacingOccurrencesOfString:@"-" withString:@"+"];
base64String = [base64String stringByReplacingOccurrencesOfString:@"_" withString:@"/"];
// @c initWithBase64EncodedString:options: requires a padded base64 string. Append enough "="
// characters to make the string a multiple of 4.
NSUInteger paddedLength = base64String.length + ((4 - (base64String.length % 4)) % 4);
base64String = [base64String stringByPaddingToLength:paddedLength
withString:@"="
startingAtIndex:0];
return [self initWithBase64EncodedString:base64String options:0];
}
@end
NS_ASSUME_NONNULL_END
@@ -18,13 +18,16 @@ package(default_visibility = ["//internal/platform/implementation/apple:__subpac
objc_library(
name = "Mediums",
srcs = [
"BLEv2/GNCBLEError.m",
"BLEv2/GNCBLEGATTCharacteristic.m",
"BLEv2/GNCBLEGATTClient.m",
"BLEv2/GNCBLEGATTServer.m",
"BLEv2/GNCBLEMedium.m",
"BLEv2/GNCCentralManager.m",
"BLEv2/GNCPeripheral.m",
"BLEv2/GNCPeripheralManager.m",
"BLEv2/NSData+GNCWebSafeBase64.m",
"Ble/GNCMBleCentral.m",
"Ble/GNCMBleConnection.m",
"Ble/GNCMBlePeripheral.m",
"Ble/GNCMBleUtils.mm",
"GNCLeaks.h",
"GNCLeaks.m",
@@ -37,13 +40,16 @@ objc_library(
"WiFiLAN/GNCWiFiLANSocket.m",
],
hdrs = [
"BLEv2/GNCBLEError.h",
"BLEv2/GNCBLEGATTCharacteristic.h",
"BLEv2/GNCBLEGATTClient.h",
"BLEv2/GNCBLEGATTServer.h",
"BLEv2/GNCBLEMedium.h",
"BLEv2/GNCCentralManager.h",
"BLEv2/GNCPeripheral.h",
"BLEv2/GNCPeripheralManager.h",
"BLEv2/NSData+GNCWebSafeBase64.h",
"Ble/GNCMBleCentral.h",
"Ble/GNCMBleConnection.h",
"Ble/GNCMBlePeripheral.h",
"Ble/GNCMBleUtils.h",
"GNCMConnection.h",
"WiFiLAN/GNCIPv4Address.h",
@@ -54,8 +60,6 @@ objc_library(
],
deps = [
"//internal/platform/implementation/apple:Shared",
"//internal/platform/implementation/apple/Mediums/Ble/Sockets:Central",
"//internal/platform/implementation/apple/Mediums/Ble/Sockets:Peripheral",
"//internal/platform/implementation/apple/Mediums/Ble/Sockets:Shared",
"//proto/mediums:ble_frames_cc_proto",
"//third_party/apple_frameworks:CoreBluetooth",
@@ -1,114 +0,0 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <CoreBluetooth/CoreBluetooth.h>
#import "internal/platform/implementation/apple/Mediums/GNCMConnection.h"
@class CBUUID;
NS_ASSUME_NONNULL_BEGIN
/**
* This handler is called on a discover when a nearby advertising endpoint is discovered.
*/
typedef void (^GNCMScanResultHandler)(NSString *peripheralID, NSData *serviceData);
/**
* This handler is called on a discovery when a nearby advertising endpoint is connected.
*/
typedef void (^GNCMGATTConnectionResultHandler)(NSError *_Nullable error);
/**
* This handler is called on a discovery when a nearby advertising endpoint is connected. The input
* is a discovered set of characteristics.
*/
typedef void (^GNCMGATTDiscoverResultHandler)(
NSOrderedSet<CBCharacteristic *> *_Nullable characteristicValues);
/**
* This handler is called by a discovering endpoint to request a connection with an an advertising
* endpoint.
*/
typedef void (^GNCMBleConnectionRequester)(NSString *serviceID,
GNCMConnectionHandler connectionHandler);
/**
* This handler is called on a discoverer when a nearby advertising endpoint is discovered.
* Call |requestConnection| to request a connection with the advertiser.
*/
typedef void (^GNCMBleRequestConnectionHandler)(GNCMBleConnectionRequester requestConnection);
/**
* GNCMBleCentral discovers devices advertising the specified service UUID via BLE (using the
* GNCMBlePeripheral class) and calls the specififed scanning result handler when one is found.
*
* This class is thread-safe. Any calls made to it (and to the objects/closures it passes back via
* callbacks) can be made from any thread/queue. Callbacks made from this class are called on the
* specified queue.
*/
@interface GNCMBleCentral : NSObject
- (instancetype)init NS_DESIGNATED_INITIALIZER;
/**
* Starts scanning with service UUID.
*
* @param serviceUUID A string that uniquely identifies the scanning services to search for.
* @param scanResultHandler The handler that is called when an endpoint advertising the service
* UUID is discovered.
* @param requestConnectionHandler The handler that is called when an endpoint is discovered.
* @param callbackQueue The queue on which all callbacks are made.
*/
- (BOOL)startScanningWithServiceUUID:(NSString *)serviceUUID
scanResultHandler:(GNCMScanResultHandler)scanResultHandler
requestConnectionHandler:(GNCMBleRequestConnectionHandler)requestConnectionHandler
callbackQueue:(nullable dispatch_queue_t)callbackQueue;
/**
* Sets up a GATT connection.
*
* @param peripheralID A string that uniquely identifies the peripheral.
* @param gattConnectionResultHandler The handler that is called when an endpoint is
* connected.
*/
- (void)connectGattServerWithPeripheralID:(NSString *)peripheralID
gattConnectionResultHandler:
(GNCMGATTConnectionResultHandler)gattConnectionResultHandler;
/**
* Discovers GATT service and its associated characteristics with values.
*
* @param serviceUUID A CBUUID for service to discover.
* @param gattCharacteristics Array of CBUUID for characteristic to discover.
* @param peripheralID A string that uniquely identifies the peripheral.
* @param gattDiscoverResultHandler This handler is called on a discovery for a discovered map of
* characteristic values when a nearby advertising endpoint is
* connected.
*/
- (void)discoverGattService:(CBUUID *)serviceUUID
gattCharacteristics:(NSArray<CBUUID *> *)characteristicUUIDs
peripheralID:(NSString *)peripheralID
gattDiscoverResultHandler:(GNCMGATTDiscoverResultHandler)gattDiscoverResultHandler;
/**
* Disconnects GATT connection.
*
* @param peripheralID A string that uniquely identifies the peripheral.
*/
- (void)disconnectGattServiceWithPeripheralID:(NSString *)peripheralID;
@end
NS_ASSUME_NONNULL_END
@@ -1,510 +0,0 @@
// Copyright 2022 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/Ble/GNCMBleCentral.h"
#import "internal/platform/implementation/apple/Mediums/Ble/GNCMBleConnection.h"
#import "internal/platform/implementation/apple/Mediums/Ble/GNCMBleUtils.h"
#import "internal/platform/implementation/apple/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.h"
#import "internal/platform/implementation/apple/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.h"
#import "internal/platform/implementation/apple/Mediums/GNCMConnection.h"
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, GNCMCentralState) {
GNCMCentralStateStopped,
GNCMCentralStateScanning,
};
typedef void (^GNCMBleCharacteristicsHandler)(NSArray<CBCharacteristic *> *characteristics,
NSError *error);
typedef void (^GNCMBleCharacteristicValueHandler)(CBCharacteristic *characteristic, NSError *error);
typedef void (^GNCIntHandler)(int);
/** This lets a GNCIntHandler call itself. */
GNCIntHandler GNCRecursiveIntHandler(void (^block)(GNCIntHandler blockSelf, int i)) {
return ^(int i) {
return block(GNCRecursiveIntHandler(block), i);
};
}
/** This represents a discovered peripheral. */
@interface GNCMPeripheralInfo : NSObject
@property(nonatomic) CBPeripheral *peripheral;
@property(nonatomic, copy) NSDictionary<NSString *, id> *advertisementData;
/** Called when characteristics are discovered. */
@property(nonatomic, nullable) GNCMBleCharacteristicsHandler charsHandler;
/** Called when a characteristic value is read. */
@property(nonatomic, nullable) GNCMBleCharacteristicValueHandler charValueHandler;
@end
@implementation GNCMPeripheralInfo
- (instancetype)initWithPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary<NSString *, id> *)advertisementData {
self = [super init];
if (self) {
_peripheral = peripheral;
_advertisementData = [advertisementData copy];
}
return self;
}
- (BOOL)isEqual:(id)object {
// There is always exactly one info object per peripheral, so compare by identity. This is needed
// for maintenance of the peripherals stored in multiple maps.
return self == object;
}
- (NSUInteger)hash {
return (NSUInteger)self;
}
@end
@interface GNCMBleCentral () <CBCentralManagerDelegate,
CBPeripheralDelegate,
GNSCentralManagerDelegate>
@end
@implementation GNCMBleCentral {
/** Service UUID the central is scanning for. */
CBUUID *_serviceUUID;
/** The scan result callback handler. */
GNCMScanResultHandler _scanResultHandler;
/** Central manager used to scan or connect to peripherals. */
CBCentralManager *_centralManager;
/** Serial background queue for |centralManager|. */
dispatch_queue_t _selfQueue;
/** Central state for stop or scanning. */
GNCMCentralState _centralState;
/** The dictionary keyed by CBPeripheral identifier to value GNCMPeripheralInfo. */
NSMutableDictionary<NSUUID *, GNCMPeripheralInfo *> *_nearbyPeripheralsByID;
/** Array of characteristic UUID used for discovering. */
NSArray<CBUUID *> *_characteristicUUIDs;
/** GATT connection result handler. */
GNCMGATTConnectionResultHandler _gattConnectionResultHandler;
/** GATT service and characteristic discovery result hanadler. */
GNCMGATTDiscoverResultHandler _gattDiscoverResultHandler;
/** The discovered characteristic values map used to callback for `_gattDiscoverResultHandler`. */
NSMutableOrderedSet<CBCharacteristic *> *_gattCharacteristicValues;
/** Central manager used for socket connection based on weave protocol. */
GNSCentralManager *_socketCentralManager;
/** A callback handler to reuqest connection on the discovered advertiser. */
GNCMBleRequestConnectionHandler _requestConnectionHandler;
/** Client callback queue. If client doesn't assign it, then use main queue. */
dispatch_queue_t _clientCallbackQueue;
/** Internal async priority queue. */
dispatch_queue_t _internalCallbackQueue;
/** Flag to disable callback for dealloc. */
BOOL _callbacksEnabled;
}
- (instancetype)init {
if (self = [super init]) {
// To make this class thread-safe, use a serial queue for all state changes, and have Core
// Bluetooth also use this queue.
_selfQueue = dispatch_queue_create("GNCCentralManagerQueue", DISPATCH_QUEUE_SERIAL);
_nearbyPeripheralsByID = [NSMutableDictionary dictionary];
_gattCharacteristicValues = [[NSMutableOrderedSet alloc] init];
_centralState = GNCMCentralStateStopped;
}
return self;
}
- (void)dealloc {
// These calls must be made on |selfQueue|. Can't capture |self| in an async block, so must use
// dispatch_sync. This means dealloc must be called from an external queue, which means |self|
// must never be captured by any escaping block used in this class.
dispatch_sync(_selfQueue, ^{
[self stopScanningInternal];
[_socketCentralManager stopNoScanMode];
_callbacksEnabled = NO;
});
}
- (BOOL)startScanningWithServiceUUID:(NSString *)serviceUUID
scanResultHandler:(GNCMScanResultHandler)scanResultHandler
requestConnectionHandler:(GNCMBleRequestConnectionHandler)requestConnectionHandler
callbackQueue:(nullable dispatch_queue_t)callbackQueue {
NSLog(@"[NEARBY] Client rquests startScanning");
_serviceUUID = [CBUUID UUIDWithString:serviceUUID];
_scanResultHandler = scanResultHandler;
_requestConnectionHandler = requestConnectionHandler;
// The client may be using the callback queue for other purposes, so wrap it with a private
// queue to know with certainty when all callbacks are done.
_clientCallbackQueue = callbackQueue ?: dispatch_get_main_queue();
_internalCallbackQueue =
dispatch_queue_create("GNCMBleCentralCallbackQueue", DISPATCH_QUEUE_PRIORITY_DEFAULT);
_callbacksEnabled = YES;
// Set up the central manager for scanning.
_centralManager =
[[CBCentralManager alloc] initWithDelegate:self
queue:_selfQueue
options:@{CBCentralManagerOptionShowPowerAlertKey : @NO}];
// Set up the central manager for the socket.
_socketCentralManager = [[GNSCentralManager alloc] initWithSocketServiceUUID:_serviceUUID
queue:_selfQueue];
_socketCentralManager.delegate = self;
[_socketCentralManager startNoScanModeWithAdvertisedServiceUUIDs:@[ _serviceUUID ]];
_centralState = GNCMCentralStateScanning;
return YES;
}
- (void)connectGattServerWithPeripheralID:(NSString *)peripheralID
gattConnectionResultHandler:
(GNCMGATTConnectionResultHandler)gattConnectionResultHandler {
_gattConnectionResultHandler = gattConnectionResultHandler;
dispatch_sync(_selfQueue, ^{
GNCMPeripheralInfo *peripheralInfo =
_nearbyPeripheralsByID[[[NSUUID alloc] initWithUUIDString:peripheralID]];
if (!peripheralInfo) return;
[_centralManager connectPeripheral:peripheralInfo.peripheral options:nil];
});
}
- (void)discoverGattService:(CBUUID *)serviceUUID
gattCharacteristics:(NSArray<CBUUID *> *)characteristicUUIDs
peripheralID:(NSString *)peripheralID
gattDiscoverResultHandler:(GNCMGATTDiscoverResultHandler)gattDiscoverResultHandler {
_gattDiscoverResultHandler = gattDiscoverResultHandler;
[_gattCharacteristicValues removeAllObjects];
dispatch_sync(_selfQueue, ^{
GNCMPeripheralInfo *peripheralInfo =
_nearbyPeripheralsByID[[[NSUUID alloc] initWithUUIDString:peripheralID]];
if (!peripheralInfo) return;
_characteristicUUIDs = [characteristicUUIDs copy];
// Start to discover service and the delegate will get its characteristics and read their values
// recursively
[peripheralInfo.peripheral discoverServices:@[ serviceUUID ]];
});
}
- (void)disconnectGattServiceWithPeripheralID:(NSString *)peripheralID {
dispatch_sync(_selfQueue, ^{
GNCMPeripheralInfo *peripheralInfo =
_nearbyPeripheralsByID[[[NSUUID alloc] initWithUUIDString:peripheralID]];
if (!peripheralInfo) return;
_gattConnectionResultHandler = nil;
_gattDiscoverResultHandler = nil;
[_centralManager cancelPeripheralConnection:peripheralInfo.peripheral];
});
}
#pragma mark CBCentralManagerDelegate
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
if (central.state == CBManagerStatePoweredOn && !central.isScanning &&
_centralState == GNCMCentralStateScanning) {
NSLog(@"[NEARBY] CBCentralManager powered on; starting scan");
[self startScanningInternal];
} else {
NSLog(@"[NEARBY] CBCentralManager not powered on; stopping scan");
[self stopScanningInternal];
}
}
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary<NSString *, id> *)advertisementData
RSSI:(NSNumber *)RSSI {
NSNumber *connectable = advertisementData[CBAdvertisementDataIsConnectable];
if (![connectable boolValue]) return;
// Look for the NC advertisement header in either the service data (from non-iOS) or the
// advertised name (from iOS).
NSData *serviceData = advertisementData[CBAdvertisementDataServiceDataKey][_serviceUUID]
?: advertisementData[CBAdvertisementDataLocalNameKey];
// Try to look up the peripheral by ID.
GNCMPeripheralInfo *info = _nearbyPeripheralsByID[peripheral.identifier];
if (!info) {
NSLog(@"[NEARBY] New peripheral: %@", peripheral);
// This is a new peripheral, so create a new peripheral info object.
info = [[GNCMPeripheralInfo alloc] initWithPeripheral:peripheral
advertisementData:advertisementData];
} else {
info.peripheral = peripheral;
}
_nearbyPeripheralsByID[peripheral.identifier] = info;
_scanResultHandler(peripheral.identifier.UUIDString, serviceData);
}
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
NSLog(@"[NEARBY] Connected to peripheral: %@", peripheral);
peripheral.delegate = self;
// Tell the caller the connection is done.
_gattConnectionResultHandler(nil);
}
- (void)centralManager:(CBCentralManager *)central
didFailToConnectPeripheral:(CBPeripheral *)peripheral
error:(nullable NSError *)error {
NSLog(@"[NEARBY] Failed to connect to peripheral: %@, error: %@", peripheral, error);
// Tell the caller the connection failed.
_gattConnectionResultHandler(error);
}
- (void)centralManager:(CBCentralManager *)central
didDisconnectPeripheral:(CBPeripheral *)peripheral
error:(nullable NSError *)error {
NSLog(@"[NEARBY] Disconnected to peripheral: %@, error: %@", peripheral, error);
}
#pragma mark CBPeripheralDelegate
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error {
GNCMPeripheralInfo *peripheralInfo = _nearbyPeripheralsByID[peripheral.identifier];
if (!peripheralInfo) return;
if (error || (peripheral.services.count == 0)) {
NSLog(@"[NEARBY] Error reading advertisement: unable to discover services.");
_gattDiscoverResultHandler(nil);
} else {
NSLog(@"[NEARBY] Discovered services for %@: %@", peripheral.name, peripheral.services);
// Helper functions for discovering characteristics and reading their values.
void (^discoverChars)(CBService *, GNCMBleCharacteristicsHandler) =
^(CBService *service, GNCMBleCharacteristicsHandler handler) {
NSAssert(!peripheralInfo.charsHandler, @"Unexpected characteristic handler");
peripheralInfo.charsHandler = handler;
// Discover all characteristics that may contain the advertisement.
[peripheral discoverCharacteristics:_characteristicUUIDs forService:service];
};
void (^readCharValue)(CBCharacteristic *, GNCMBleCharacteristicValueHandler) =
^(CBCharacteristic *characteristic, GNCMBleCharacteristicValueHandler handler) {
NSAssert(!peripheralInfo.charValueHandler, @"Unexpected characteristic value handler");
peripheralInfo.charValueHandler = handler;
[peripheral readValueForCharacteristic:characteristic];
};
// Multiple services may have the same UUID, so find the right service by searching for the
// characteristic containing the advertisement with a matching service ID hash.
__weak __typeof__(self) weakSelf = self;
void (^tryService)(int) = GNCRecursiveIntHandler(^(GNCIntHandler tryService, int serviceIndex) {
__strong __typeof__(self) strongSelf = weakSelf;
if (!strongSelf) return;
// We've tried all the services, report the discovered characteristics and their values.
if (serviceIndex == peripheral.services.count) {
NSLog(@"[NEARBY] Done traversing all services or no services to traverse.");
_gattDiscoverResultHandler(_gattCharacteristicValues);
if (_gattCharacteristicValues.count > 0) {
CBCharacteristic *characteristic = [_gattCharacteristicValues objectAtIndex:0];
[strongSelf completeGATTReadWithData:characteristic.value
forPeripheralInfo:peripheralInfo];
}
return;
}
NSLog(@"[NEARBY] Trying service: %@", peripheral.services[serviceIndex]);
discoverChars(
peripheral.services[serviceIndex], ^(NSArray<CBCharacteristic *> *chars, NSError *error) {
void (^tryNextService)() = ^{
tryService(serviceIndex + 1);
};
// If there was an error or there are no characteristics on this service, try next one.
if (error || (chars.count == 0)) {
tryNextService();
return;
}
// Read each characteristic.
void (^tryChar)(int) = GNCRecursiveIntHandler(^(GNCIntHandler tryChar, int charIndex) {
// We've tried all characteristics on this service without error, try next service.
if (charIndex == chars.count) {
NSLog(@"[NEARBY] No matching advertisement found");
tryNextService();
return;
}
NSLog(@"[NEARBY] Trying characteristic: %@", chars[charIndex]);
readCharValue(chars[charIndex], ^(CBCharacteristic *characteristic, NSError *error) {
if (error) {
tryNextService();
} else {
// We've found the characteristic and its non-nil value. Store it.
if (characteristic.value.length != 0) {
[_gattCharacteristicValues addObject:characteristic];
}
tryChar(charIndex + 1);
}
});
});
// Start searching the characteristics for the current service.
tryChar(0);
});
});
// Start searching the services.
tryService(0);
}
}
- (void)peripheral:(CBPeripheral *)peripheral
didDiscoverCharacteristicsForService:(CBService *)service
error:(nullable NSError *)error {
NSLog(@"[NEARBY] Discovered characteristics: %@ error: %@", service.characteristics, error);
GNCMPeripheralInfo *peripheralInfo = _nearbyPeripheralsByID[peripheral.identifier];
if (!peripheralInfo) return;
GNCMBleCharacteristicsHandler charsHandler = peripheralInfo.charsHandler;
peripheralInfo.charsHandler = nil;
charsHandler(service.characteristics, error);
}
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(nullable NSError *)error {
NSLog(@"[NEARBY] Read characteristic value: %@ error: %@", characteristic, error);
GNCMPeripheralInfo *peripheralInfo = _nearbyPeripheralsByID[peripheral.identifier];
if (!peripheralInfo) return;
GNCMBleCharacteristicValueHandler valueHandler = peripheralInfo.charValueHandler;
peripheralInfo.charValueHandler = nil;
valueHandler(characteristic, error);
}
#pragma mark GNSCentralManagerDelegate
- (void)centralManager:(GNSCentralManager *)centralManager
didDiscoverPeer:(GNSCentralPeerManager *)centralPeerManager
advertisementData:(nullable NSDictionary<NSString *, id> *)advertisementData {
if (!_callbacksEnabled) return;
// Retrieve the cached peripheral info.
NSUUID *peerId = centralPeerManager.identifier;
GNCMPeripheralInfo *peripheralInfo = _nearbyPeripheralsByID[peerId];
if (!peripheralInfo) return;
__weak __typeof__(self) weakSelf = self;
[self callbackAsync:^{
_requestConnectionHandler(^(NSString *serviceID, GNCMConnectionHandler connectionHandler) {
__strong __typeof__(self) strongSelf = weakSelf;
if (!strongSelf) return;
void (^callConnectionHandler)(GNSSocket *__nullable) = ^(GNSSocket *__nullable socket) {
__strong __typeof__(self) strongSelf = weakSelf;
if (!strongSelf->_callbacksEnabled) return;
[strongSelf callbackAsync:^{
if (!socket) {
NSLog(@"[NEARBY] Central failed to create BLE socket");
connectionHandler(nil);
} else {
GNCMBleConnection *connection =
[GNCMBleConnection connectionWithSocket:socket
serviceID:serviceID
expectedIntroPacket:NO
callbackQueue:strongSelf->_clientCallbackQueue];
connection.connectionHandlers = connectionHandler(connection);
}
}];
};
dispatch_async(strongSelf->_selfQueue, ^{
// A connection is being requested, so establish a BLE socket. Make sure to use the most
// up-to-date GNSCentralPeerManager in case the MAC address rotated. The cached
// peripheral info should be the single source of truth for which MAC address to use.
GNSCentralPeerManager *updatedCentralPeerManager =
[centralManager retrieveCentralPeerWithIdentifier:peripheralInfo.peripheral.identifier];
if (!updatedCentralPeerManager) {
callConnectionHandler(nil);
}
// Make a socket connection.
[updatedCentralPeerManager
socketWithPairingCharacteristic:NO
completion:^(GNSSocket *socket, NSError *error) {
__strong __typeof__(self) strongSelf = weakSelf;
if (!strongSelf) return;
dispatch_async(strongSelf->_selfQueue, ^{
if (!error) {
// Call the connection handler when the socket has
// connected or fails to connect.
GNCMWaitForConnection(socket, ^(BOOL didConnect) {
callConnectionHandler(didConnect ? socket : nil);
});
} else {
callConnectionHandler(nil);
}
});
}];
});
});
}];
}
- (void)centralManagerDidUpdateBleState:(GNSCentralManager *)centralManager {
// No op.
}
#pragma mark Private
/** This method assumes it's being called on selfQueue. */
- (void)completeGATTReadWithData:(NSData *)advertisement
forPeripheralInfo:(GNCMPeripheralInfo *)peripheralInfo {
NSAssert(peripheralInfo, @"Nil peripheralInfo");
if (peripheralInfo) {
CBPeripheral *peripheral = peripheralInfo.peripheral;
NSUUID *peripheralID = peripheral.identifier;
// Store the data for the socket callback, and report the peripheral to the socket library.
[_socketCentralManager retrievePeripheralWithIdentifier:peripheralID
advertisementData:peripheralInfo.advertisementData];
}
}
/** Signals the central manager to start scanning. Must be called on _selfQueue. */
- (void)startScanningInternal {
if (![_centralManager isScanning]) {
NSLog(@"[NEARBY] startScanningInternal");
[_centralManager
scanForPeripheralsWithServices:@[ _serviceUUID ]
options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES}];
}
}
/** Signals the central manager to stop scanning. Must be called on _selfQueue */
- (void)stopScanningInternal {
if ([_centralManager isScanning]) {
NSLog(@"[NEARBY] stopScanningInternal");
_centralState = GNCMCentralStateStopped;
[_centralManager stopScan];
}
}
/** Calls the specified block on the callback queue. */
- (void)callbackAsync:(dispatch_block_t)block {
dispatch_queue_t clientCallbackQueue = _clientCallbackQueue; // don't capture |self|
dispatch_async(_internalCallbackQueue, ^{
dispatch_sync(clientCallbackQueue, block);
});
}
@end
NS_ASSUME_NONNULL_END
@@ -1,79 +0,0 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <Foundation/Foundation.h>
#import "internal/platform/implementation/apple/Mediums/GNCMConnection.h"
@class CBCharacteristic;
@class CBUUID;
NS_ASSUME_NONNULL_BEGIN
/**
* GNCMBlePeripheral advertises the specified service UUID via Ble for the purpose of being
* discovered by a central using the GNCMBleCentral class.
*
* This class is thread-safe. Any calls made to it (and to the objects/closures it passes back via
* callbacks) can be made from any thread/queue. Callbacks made from this class are called on the
* specified queue.
*/
@interface GNCMBlePeripheral : NSObject
- (instancetype)init NS_DESIGNATED_INITIALIZER;
/**
* Adds GATT CBService.
*
* @param serviceUUID A GATT service ID to advertise for.
*/
- (void)addCBServiceWithUUID:(CBUUID *)serviceUUID;
/**
* Adds GATT CBCharacteristic.
*
* @param characteristic A characteristic CBUUID.
*/
- (void)addCharacteristic:(CBCharacteristic *)characteristic;
/**
* Updates GATT CBCharacteristic with value.
*
* @param value The NSData to advertise.
* @param characteristicUUID A characteristic CBUUID.
*/
- (void)updateValue:(NSData *)value forCharacteristic:(CBUUID *)characteristicUUID;
/**
* Stops GATT server service.
*/
- (void)stopGATTService;
/**
* Starts advertising with service UUID and advertisement data.
*
* @param serviceUUID A string that uniquely identifies the advertised service to search for.
* @param advertisementData The data to advertise.
* @param endpointconnectedHandler The handler that is called when a discoverer connects.
* @param callbackQueue The queue on which all callbacks are made. If |callbackQueue| is not
* provided, then the main queue is used in the function.
*/
- (BOOL)startAdvertisingWithServiceUUID:(NSString *)serviceUUID
advertisementData:(NSData *)advertisementData
endpointConnectedHandler:(GNCMConnectionHandler)endpointConnectedHandler
callbackQueue:(nullable dispatch_queue_t)callbackQueue;
@end
NS_ASSUME_NONNULL_END
@@ -1,293 +0,0 @@
// Copyright 2022 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/Ble/GNCMBlePeripheral.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import "internal/platform/implementation/apple/GNCUtils.h"
#import "internal/platform/implementation/apple/Mediums/Ble/GNCMBleConnection.h"
#import "internal/platform/implementation/apple/Mediums/Ble/GNCMBleUtils.h"
#import "internal/platform/implementation/apple/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager.h"
#import "internal/platform/implementation/apple/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager.h"
#import "internal/platform/implementation/apple/Mediums/GNCMConnection.h"
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, GNCMPeripheralState) {
GNCMPeripheralStateStopped,
GNCMPeripheralStateAdvertising,
};
@interface GNCMBlePeripheral () <CBPeripheralManagerDelegate>
@end
@implementation GNCMBlePeripheral {
/** Service UUID for advertisement. */
CBMutableService *_advertisementService;
/** GATT service for GATT connection. */
CBMutableService *_GATTService;
/** GATT characteristics for GATT connection. */
NSMutableArray<CBCharacteristic *> *_gattCharacteristics;
/** CBUUID characteristic to NSData value dictionary for GATT connection. */
NSMutableDictionary<CBUUID *, NSData *> *_gattCharacteristicValues;
/** Data to be advertised. */
NSData *_advertisementData;
/** Peripheral manager used to advertise or connect to peripherals. */
CBPeripheralManager *_peripheralManager;
/** Serial background queue for |peripheralManager|. */
dispatch_queue_t _selfQueue;
/** Peripheral state for stop or advertising. */
GNCMPeripheralState _state;
/** Peripheral manager used for socket connection based on weave protocol. */
GNSPeripheralManager *_socketPeripheralManager;
/** Peripheral service manager used to manage one BLE service. */
GNSPeripheralServiceManager *_socketPeripheralServiceManager;
/** Client callback queue. If client doesn't assign it, then use main queue. */
dispatch_queue_t _clientCallbackQueue;
/** Internal async priority queue. */
dispatch_queue_t _internalCallbackQueue;
/** Flag to disable callback for dealloc. */
BOOL _callbacksEnabled;
}
- (instancetype)init {
if (self = [super init]) {
// To make this class thread-safe, use a serial queue for all state changes, and have Core
// Bluetooth also use this queue.
_selfQueue = dispatch_queue_create("GNCPeripheralManagerQueue", DISPATCH_QUEUE_SERIAL);
_state = GNCMPeripheralStateStopped;
}
return self;
}
- (void)dealloc {
// These calls must be made on |selfQueue|. Can't capture |self| in an async block, so must use
// dispatch_sync. This means delloc must be called from an external queue, which means |self|
// must never be captured by any escaping block used in this class.
dispatch_sync(_selfQueue, ^{
[self stopAdvertisingInternal];
_callbacksEnabled = NO;
});
}
- (void)addCBServiceWithUUID:(CBUUID *)serviceUUID {
if (!_GATTService) {
// If it has been called, then don't do it again. Initialize one time.
_GATTService = [[CBMutableService alloc] initWithType:serviceUUID primary:YES];
_gattCharacteristics = [[NSMutableArray alloc] init];
_gattCharacteristicValues = [[NSMutableDictionary alloc] init];
}
}
- (void)addCharacteristic:(CBCharacteristic *)characteristic {
if (_gattCharacteristics) {
[_gattCharacteristics addObject:characteristic];
}
if (_gattCharacteristicValues) {
[_gattCharacteristicValues setObject:[[NSData alloc] init] forKey:characteristic.UUID];
}
}
- (void)updateValue:(NSData *)value forCharacteristic:(CBUUID *)characteristicUUID {
if ([_gattCharacteristicValues objectForKey:characteristicUUID]) {
[_gattCharacteristicValues setObject:value forKey:characteristicUUID];
}
}
- (void)stopGATTService {
if (!_GATTService) return;
dispatch_sync(_selfQueue, ^{
[_peripheralManager removeService:_GATTService];
});
}
- (BOOL)startAdvertisingWithServiceUUID:(NSString *)serviceUUID
advertisementData:(NSData *)advertisementData
endpointConnectedHandler:(GNCMConnectionHandler)endpointConnectedHandler
callbackQueue:(nullable dispatch_queue_t)callbackQueue {
NSLog(@"[NEARBY] Client rquests startAdvertising");
// The client may be using the callback queue for other purposes, so wrap it with a private
// queue to know with certainty when all callbacks are done.
_clientCallbackQueue = callbackQueue ?: dispatch_get_main_queue();
_internalCallbackQueue =
dispatch_queue_create("GNCMBlePeripheralCallbackQueue", DISPATCH_QUEUE_PRIORITY_DEFAULT);
_callbacksEnabled = YES;
_advertisementService = [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:serviceUUID]
primary:YES];
_advertisementData = [advertisementData copy];
// To make this class thread-safe, use a serial queue for all state changes, and have Core
// Bluetooth also use this queue.
_selfQueue = dispatch_queue_create("GNCPeripheralManagerQueue", DISPATCH_QUEUE_SERIAL);
__weak __typeof__(self) weakSelf = self;
// Set up the peripheral manager for the socket. This must be done before creating the
// peripheral manager for the advertisement data because it's started/stopped in the
// -peripheralManagerDidUpdateState: callback.
_socketPeripheralServiceManager = [[GNSPeripheralServiceManager alloc]
initWithBleServiceUUID:_advertisementService.UUID
addPairingCharacteristic:NO
shouldAcceptSocketHandler:^BOOL(GNSSocket *socket) {
// Call the connection handler when the socket has connected or fails to connect.
GNCMWaitForConnection(socket, ^(BOOL didConnect) {
[weakSelf establishConnectionWithSocket:socket
didConnect:didConnect
endpointConnectedHandler:endpointConnectedHandler];
});
return YES;
}
queue:_selfQueue];
_socketPeripheralManager = [[GNSPeripheralManager alloc] initWithAdvertisedName:nil
restoreIdentifier:nil
queue:_selfQueue];
[_socketPeripheralManager addPeripheralServiceManager:_socketPeripheralServiceManager
bleServiceAddedCompletion:^(NSError *error) {
NSLog(@"Failed to add service");
}];
// Set up the peripheral manager for the advertisement data.
_peripheralManager = [[CBPeripheralManager alloc]
initWithDelegate:self
queue:_selfQueue
options:@{CBPeripheralManagerOptionShowPowerAlertKey : @NO}];
if (_GATTService) {
if (_gattCharacteristics && _gattCharacteristics.count > 0) {
_GATTService.characteristics = _gattCharacteristics;
}
}
_state = GNCMPeripheralStateAdvertising;
return YES;
}
#pragma mark CBPeripheralManagerDelegate
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
NSLog(@"[NEARBY] peripheralManagerDidUpdateState %li", (long)peripheral.state);
if (peripheral.state == CBManagerStatePoweredOn && !peripheral.isAdvertising &&
_state == GNCMPeripheralStateAdvertising) {
NSLog(@"[NEARBY] CBPeripheralManager powered on; starting advertising");
[_socketPeripheralManager start];
[self startAdvertisingInternal];
} else {
NSLog(@"[NEARBY] CBPeripheralManager not powered on; stopping advertising");
[self stopAdvertisingInternal];
}
}
- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral
error:(nullable NSError *)error {
if (error) {
NSLog(@"[NEARBY] Error starting advertising: %@,", [error localizedDescription]);
return;
}
if (_peripheralManager.state != CBManagerStatePoweredOn) {
NSLog(@"[NEARBY] Error starting advertising: peripheral manager not on!");
return;
}
NSLog(@"[NEARBY] Peripheral manager started advertising");
}
- (void)peripheralManager:(CBPeripheralManager *)peripheral
didReceiveReadRequest:(CBATTRequest *)request {
NSLog(@"[NEARBY] peripheralManager:didReceiveReadRequest");
// This is called when a central asks to read a characteristic's value.
CBATTError error = CBATTErrorAttributeNotFound;
NSData *value = _gattCharacteristicValues[request.characteristic.UUID];
if (value != nil && value.length > 0) {
if (request.offset > value.length) {
error = CBATTErrorInvalidOffset;
} else {
// Reply with the advertisement data.
NSRange rangeFromOffset = NSMakeRange(request.offset, value.length - request.offset);
request.value = [value subdataWithRange:rangeFromOffset];
error = CBATTErrorSuccess;
}
}
[_peripheralManager respondToRequest:request withResult:error];
}
#pragma mark Private
/** Signals the peripheral manager to start advertising. Must be called on _selfQueue */
- (void)startAdvertisingInternal {
if (![_peripheralManager isAdvertising]) {
NSLog(@"[NEARBY] startAdvertisingInternal");
if (_GATTService) {
[_peripheralManager addService:_GATTService];
}
[_peripheralManager startAdvertising:@{
CBAdvertisementDataServiceUUIDsKey : @[ _advertisementService.UUID ],
CBAdvertisementDataLocalNameKey : _advertisementData
}];
}
}
/** Signals the peripheral manager to stop advertising. Must be called on _selfQueue */
- (void)stopAdvertisingInternal {
if ([_peripheralManager isAdvertising]) {
NSLog(@"[NEARBY] stopAdvertisingInternal");
_state = GNCMPeripheralStateStopped;
if (_GATTService) {
[_peripheralManager removeService:_GATTService];
}
[_peripheralManager stopAdvertising];
}
}
/**
* Connects with socket and callback the |GNCMBleConnection| is established or nil if it is not.
*/
- (void)establishConnectionWithSocket:(GNSSocket *)socket
didConnect:(BOOL)didConnect
endpointConnectedHandler:(GNCMConnectionHandler)endpointConnectedHandler {
if (!_callbacksEnabled) {
return;
}
[self callbackAsync:^{
if (!didConnect) {
NSLog(@"[NEARBY] Peripheral failed to create BLE socket");
endpointConnectedHandler(nil);
} else {
GNCMBleConnection *connection = [GNCMBleConnection connectionWithSocket:socket
serviceID:nil
expectedIntroPacket:YES
callbackQueue:_clientCallbackQueue];
connection.connectionHandlers = endpointConnectedHandler(connection);
}
}];
}
/**
* Calls the specified block on the callback queue, preventing it from being dispatched to the
* client callback queue when callbacks are disabled. And without capturing |self|, since
* callbacks are disabled in dealloc.
*/
- (void)callbackAsync:(dispatch_block_t)block {
dispatch_queue_t clientCallbackQueue = _clientCallbackQueue; // don't capture |self|
dispatch_async(_internalCallbackQueue, ^{
dispatch_sync(clientCallbackQueue, block);
});
}
@end
NS_ASSUME_NONNULL_END
@@ -24,16 +24,23 @@ objc_library(
testonly = True,
srcs = [
"GNCBLEGATTCharacteristicTest.mm",
"GNCBLEGATTClient+Testing.h",
"GNCBLEGATTClientTest.m",
"GNCBLEGATTServer+Testing.h",
"GNCBLEGATTServerTest.m",
"GNCBLEMedium+Testing.h",
"GNCBLEMediumTest.m",
"GNCBLEUtilsTest.mm",
"GNCBleTest.mm",
"GNCBluetoothAdapterTest.mm",
"GNCCryptoTest.mm",
"GNCFakeCentralManager.h",
"GNCFakeCentralManager.m",
"GNCFakePeripheral.h",
"GNCFakePeripheral.m",
"GNCFakePeripheralManager.h",
"GNCFakePeripheralManager.m",
"GNCIPAddressTest.mm",
"GNCMultiThreadExecutorTest.mm",
"GNCPlatformTest.mm",
"GNCScheduledExecutorTest.mm",
"GNCSingleThreadExecutorTest.mm",
"GNCUtilsTest.mm",
@@ -44,7 +51,8 @@ objc_library(
"//internal/platform/implementation:comm",
"//internal/platform/implementation:platform",
"//internal/platform/implementation:types",
"//internal/platform/implementation/apple",
"//internal/platform/implementation/apple", # buildcleaner: keep
"//internal/platform/implementation/apple:ble_v2",
"//internal/platform/implementation/apple/Mediums",
"//third_party/apple_frameworks:CoreBluetooth",
"//third_party/apple_frameworks:Foundation",
@@ -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 {
@@ -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 <Foundation/Foundation.h>
@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<GNCPeripheral>)peripheral
queue:(nullable dispatch_queue_t)queue;
@end
NS_ASSUME_NONNULL_END
@@ -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 <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#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
@@ -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 <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h"
#import "internal/platform/implementation/apple/Tests/GNCBLEGATTServer+Testing.h"
#import "internal/platform/implementation/apple/Tests/GNCFakePeripheralManager.h"
@@ -42,17 +42,26 @@ static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-00000000
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Create characteristic."];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
GNCBLEGATTCharacteristic *characteristic =
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
[gattServer
createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
XCTAssertNotNil(characteristic);
XCTAssertNil(error);
XCTAssertEqual(fakePeripheralManager.services.count, 1);
XCTAssertEqual(fakePeripheralManager.services[0].characteristics.count, 1);
[expectation fulfill];
}];
XCTAssertNotNil(characteristic);
XCTAssertEqual(fakePeripheralManager.services.count, 1);
XCTAssertEqual(fakePeripheralManager.services[0].characteristics.count, 1);
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testCreateMultipleCharacteristicsForOneService {
@@ -63,41 +72,132 @@ static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-00000000
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
XCTestExpectation *expectation1 =
[[XCTestExpectation alloc] initWithDescription:@"Create characteristic 1."];
XCTestExpectation *expectation2 =
[[XCTestExpectation alloc] initWithDescription:@"Create characteristic 2."];
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];
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID1
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
XCTAssertNotNil(characteristic);
XCTAssertNil(error);
[expectation1 fulfill];
}];
XCTAssertNotNil(characteristic1);
XCTAssertNotNil(characteristic2);
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID2
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
XCTAssertNotNil(characteristic);
XCTAssertNil(error);
[expectation2 fulfill];
}];
[self waitForExpectations:@[ expectation1, expectation2 ] timeout:3];
XCTAssertEqual(fakePeripheralManager.services.count, 1);
XCTAssertEqual(fakePeripheralManager.services[0].characteristics.count, 2);
}
- (void)testCreateDuplicateCharacteristics {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Create characteristic."];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID1 = [CBUUID UUIDWithString:kCharacteristicUUID1];
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID1
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead
completionHandler:nil];
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID1
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
XCTAssertNil(characteristic);
XCTAssertNotNil(error);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
XCTAssertEqual(fakePeripheralManager.services.count, 1);
XCTAssertEqual(fakePeripheralManager.services[0].characteristics.count, 1);
}
- (void)testCreateDuplicatePendingCharacteristics {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Create characteristic."];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID1 = [CBUUID UUIDWithString:kCharacteristicUUID1];
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID1
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead
completionHandler:nil];
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID1
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
XCTAssertNil(characteristic);
XCTAssertNotNil(error);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testCreateCharacteristicNotPoweredOn {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Create characteristic."];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
GNCBLEGATTCharacteristic *characteristic =
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
// The error occurs after completion, so its expected to fail
// silently.
XCTAssertNotNil(characteristic);
XCTAssertNil(error);
[expectation fulfill];
}];
XCTAssertNil(characteristic);
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testCreateCharacteristicServiceFailure {
@@ -109,101 +209,25 @@ static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-00000000
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];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Create characteristic."];
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
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
properties:CBCharacteristicPropertyRead
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
// The error occurs after completion, so its expected to fail
// silently.
XCTAssertNotNil(characteristic);
XCTAssertNil(error);
[expectation fulfill];
}];
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);
[self waitForExpectations:@[ expectation ] timeout:3];
}
#pragma mark - Read Request
@@ -216,15 +240,25 @@ static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-00000000
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Create and update characteristic."];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
GNCBLEGATTCharacteristic *characteristic =
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
[gattServer updateCharacteristic:characteristic
value:[NSData data]
completionHandler:^(NSError *error) {
[expectation fulfill];
}];
}];
[gattServer updateCharacteristic:characteristic value:[NSData data]];
[self waitForExpectations:@[ expectation ] timeout:3];
[fakePeripheralManager
simulatePeripheralManagerDidReceiveReadRequestForService:serviceUUID
@@ -242,15 +276,25 @@ static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-00000000
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Create and update characteristic."];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
GNCBLEGATTCharacteristic *characteristic =
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
[gattServer updateCharacteristic:characteristic
value:[NSData data]
completionHandler:^(NSError *error) {
[expectation fulfill];
}];
}];
[gattServer updateCharacteristic:characteristic value:[NSData data]];
[self waitForExpectations:@[ expectation ] timeout:3];
CBUUID *invalidServiceUUID = [CBUUID UUIDWithString:kServiceUUID2];
@@ -269,15 +313,25 @@ static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-00000000
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Create and update characteristic."];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID1];
CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID1];
GNCBLEGATTCharacteristic *characteristic =
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead];
[gattServer createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:CBAttributePermissionsReadable
properties:CBCharacteristicPropertyRead
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
[gattServer updateCharacteristic:characteristic
value:[NSData data]
completionHandler:^(NSError *error) {
[expectation fulfill];
}];
}];
[gattServer updateCharacteristic:characteristic value:[NSData data]];
[self waitForExpectations:@[ expectation ] timeout:3];
CBUUID *invalidCharacteristicUUID =
[CBUUID UUIDWithString:kCharacteristicUUID2];
@@ -311,12 +365,18 @@ static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-00000000
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
BOOL success = [gattServer startAdvertisingData:@{}];
XCTAssertTrue(success);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqual(data, nil);
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Start advertising."];
[gattServer startAdvertisingData:@{}
completionHandler:^(NSError *error) {
XCTAssertNotNil(error);
XCTAssertFalse(fakePeripheralManager.isAdvertising);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testStartAdvertisingEmptyServiceData {
@@ -326,16 +386,22 @@ static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-00000000
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
BOOL success = [gattServer startAdvertisingData:@{
[CBUUID UUIDWithString:@"FEF3"] : [NSData data],
}];
XCTAssertTrue(success);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey], @"");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF3"]);
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Start advertising."];
[gattServer startAdvertisingData:@{[CBUUID UUIDWithString:@"FEF3"] : [NSData data]}
completionHandler:^(NSError *error) {
XCTAssertNil(error);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey], @"");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF3"]);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testStartAdvertisingShortServiceData {
@@ -345,16 +411,24 @@ static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-00000000
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
BOOL success = [gattServer startAdvertisingData:@{
[CBUUID UUIDWithString:@"FEF3"] : [@"0123" dataUsingEncoding:NSUTF8StringEncoding],
}];
XCTAssertTrue(success);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey], @"MDEyMw");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF3"]);
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Start advertising."];
[gattServer startAdvertisingData:@{
[CBUUID UUIDWithString:@"FEF3"] : [@"0123" dataUsingEncoding:NSUTF8StringEncoding],
}
completionHandler:^(NSError *error) {
XCTAssertNil(error);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey], @"MDEyMw");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF3"]);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testStartAdvertising22ByteServiceData {
@@ -364,16 +438,25 @@ static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-00000000
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
BOOL success = [gattServer startAdvertisingData:@{
[CBUUID UUIDWithString:@"FEF3"] : [@"0123456789012345" dataUsingEncoding:NSUTF8StringEncoding],
}];
XCTAssertTrue(success);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey], @"MDEyMzQ1Njc4OTAxMjM0NQ");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF3"]);
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Start advertising."];
[gattServer startAdvertisingData:@{
[CBUUID UUIDWithString:@"FEF3"] : [@"0123456789012345" dataUsingEncoding:NSUTF8StringEncoding],
}
completionHandler:^(NSError *error) {
XCTAssertNil(error);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey],
@"MDEyMzQ1Njc4OTAxMjM0NQ");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF3"]);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testStartAdvertisingLongServiceData {
@@ -383,17 +466,26 @@ static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-00000000
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
BOOL success = [gattServer startAdvertisingData:@{
[CBUUID UUIDWithString:@"FEF3"] :
[@"012345678901234567890123456789" dataUsingEncoding:NSUTF8StringEncoding],
}];
XCTAssertTrue(success);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey], @"MDEyMzQ1Njc4OTAxMjM0NT");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF3"]);
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Start advertising."];
[gattServer
startAdvertisingData:@{
[CBUUID UUIDWithString:@"FEF3"] :
[@"012345678901234567890123456789" dataUsingEncoding:NSUTF8StringEncoding],
}
completionHandler:^(NSError *error) {
XCTAssertNil(error);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey], @"MDEyMzQ1Njc4OTAxMjM0NT");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF3"]);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testStartAdvertisingWithEmojiServiceData {
@@ -403,16 +495,25 @@ static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-00000000
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
BOOL success = [gattServer startAdvertisingData:@{
[CBUUID UUIDWithString:@"FEF3"] : [@"😁❤️🤡" dataUsingEncoding:NSUTF8StringEncoding],
}];
XCTAssertTrue(success);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey], @"8J-YgeKdpO-4j_CfpKE");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF3"]);
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Start advertising."];
[gattServer
startAdvertisingData:@{
[CBUUID UUIDWithString:@"FEF3"] : [@"😁❤️🤡" dataUsingEncoding:NSUTF8StringEncoding],
}
completionHandler:^(NSError *error) {
XCTAssertNil(error);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey], @"8J-YgeKdpO-4j_CfpKE");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF3"]);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testStartAdvertisingMultipleServices {
@@ -422,15 +523,23 @@ static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-00000000
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
BOOL success = [gattServer startAdvertisingData:@{
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Start advertising."];
[gattServer startAdvertisingData:@{
[CBUUID UUIDWithString:@"FEF3"] :
[@"012345678901234567890123456789" dataUsingEncoding:NSUTF8StringEncoding],
[CBUUID UUIDWithString:@"FEF4"] :
[@"012345678901234567890123456789" dataUsingEncoding:NSUTF8StringEncoding],
}];
}
completionHandler:^(NSError *error) {
XCTAssertNotNil(error);
XCTAssertFalse(fakePeripheralManager.isAdvertising);
[expectation fulfill];
}];
XCTAssertFalse(success);
XCTAssertFalse(fakePeripheralManager.isAdvertising);
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testStartAdvertisingNotPoweredOn {
@@ -439,10 +548,18 @@ static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-00000000
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
BOOL success = [gattServer startAdvertisingData:@{}];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Start advertising."];
XCTAssertFalse(success);
XCTAssertFalse(fakePeripheralManager.isAdvertising);
[gattServer startAdvertisingData:@{[CBUUID UUIDWithString:@"FEF3"] : [NSData data]}
completionHandler:^(NSError *error) {
// The error occurs after completion, so its expected to fail silently.
XCTAssertNil(error);
XCTAssertFalse(fakePeripheralManager.isAdvertising);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testStartAdvertisingStartFailure {
@@ -455,10 +572,86 @@ static NSString *const kCharacteristicUUID2 = @"00000000-0000-3000-8000-00000000
code:0
userInfo:nil];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
BOOL success = [gattServer startAdvertisingData:@{}];
XCTAssertFalse(success);
XCTAssertFalse(fakePeripheralManager.isAdvertising);
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Start advertising."];
[gattServer startAdvertisingData:@{[CBUUID UUIDWithString:@"FEF3"] : [NSData data]}
completionHandler:^(NSError *error) {
// The error occurs after completion, so its expected to fail silently.
XCTAssertNil(error);
XCTAssertFalse(fakePeripheralManager.isAdvertising);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testStartAdvertisingAlreadyAdvertising {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
fakePeripheralManager.didStartAdvertisingError = [NSError errorWithDomain:@"fake"
code:0
userInfo:nil];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Start advertising."];
[gattServer startAdvertisingData:@{[CBUUID UUIDWithString:@"FEF3"] : [NSData data]}
completionHandler:nil];
[gattServer startAdvertisingData:@{[CBUUID UUIDWithString:@"FEF4"] : [NSData data]}
completionHandler:^(NSError *error) {
XCTAssertNotNil(error);
XCTAssertFalse(fakePeripheralManager.isAdvertising);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testStartStopStartAdvertising {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEGATTServer *gattServer =
[[GNCBLEGATTServer alloc] initWithPeripheralManager:fakePeripheralManager];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Start advertising."];
[gattServer startAdvertisingData:@{[CBUUID UUIDWithString:@"FEF3"] : [NSData data]}
completionHandler:^(NSError *error) {
XCTAssertNil(error);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data = fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey], @"");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF3"]);
[gattServer stopAdvertisingWithCompletionHandler:^(NSError *error) {
XCTAssertNil(error);
XCTAssertFalse(fakePeripheralManager.isAdvertising);
[gattServer
startAdvertisingData:@{[CBUUID UUIDWithString:@"FEF4"] : [NSData data]}
completionHandler:^(NSError *error) {
XCTAssertNil(error);
XCTAssertTrue(fakePeripheralManager.isAdvertising);
NSDictionary<NSString *, id> *data =
fakePeripheralManager.advertisementData;
XCTAssertEqualObjects(data[CBAdvertisementDataLocalNameKey], @"");
XCTAssertEqualObjects(data[CBAdvertisementDataServiceUUIDsKey][0],
[CBUUID UUIDWithString:@"FEF4"]);
[expectation fulfill];
}];
}];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
@end
@@ -0,0 +1,42 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEMedium.h"
#import <Foundation/Foundation.h>
@protocol GNCCentralManager;
NS_ASSUME_NONNULL_BEGIN
@interface GNCBLEMedium (Testing)
/**
* Creates a BLE Medium with a provided central manager.
*
* This is only exposed for testing and can be used to inject a fake central manager.
*
* @param centralManager The central manager instance.
* @param queue The queue to run on, this must match the queue that the central manager's delegate
* is running on. Defaults to the main queue when @c nil.
*/
- (instancetype)initWithCentralManager:(id<GNCCentralManager>)centralManager
queue:(nullable dispatch_queue_t)queue;
- (NSDictionary<CBUUID *, NSData *> *)decodeAdvertisementData:
(NSDictionary<NSString *, id> *)advertisementData;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,340 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEMedium.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h"
#import "internal/platform/implementation/apple/Tests/GNCBLEMedium+Testing.h"
#import "internal/platform/implementation/apple/Tests/GNCFakeCentralManager.h"
#import "internal/platform/implementation/apple/Tests/GNCFakePeripheral.h"
static NSString *const kServiceUUID = @"0000FEF3-0000-1000-8000-00805F9B34FB";
@interface GNCBLEMediumTest : XCTestCase
@end
@implementation GNCBLEMediumTest
#pragma mark - Supports Extended Advertisements
- (void)testSupportsExtendedAdvertisements {
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
XCTAssertFalse([medium supportsExtendedAdvertisements]);
}
#pragma mark - Start Scanning
- (void)testStartScanning {
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
XCTestExpectation *startScanningExpectation =
[[XCTestExpectation alloc] initWithDescription:@"Start scanning."];
XCTestExpectation *advertisementFoundExpectation =
[[XCTestExpectation alloc] initWithDescription:@"Advertisement found."];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID];
[fakeCentralManager simulateCentralManagerDidUpdateState:CBManagerStatePoweredOn];
[medium startScanningForService:serviceUUID
advertisementFoundHandler:^(id<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *data) {
NSDictionary<CBUUID *, NSData *> *expected = @{
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
};
XCTAssertEqualObjects(expected, data);
[advertisementFoundExpectation fulfill];
}
completionHandler:^(NSError *error) {
XCTAssertNil(error);
[startScanningExpectation fulfill];
}];
[self waitForExpectations:@[ startScanningExpectation ] timeout:3];
XCTAssertEqualObjects(@[ serviceUUID ], fakeCentralManager.serviceUUIDs);
[fakeCentralManager
simulateCentralManagerDidDiscoverPeripheral:[[GNCFakePeripheral alloc] init]
advertisementData:@{
CBAdvertisementDataLocalNameKey : @"dGVzdA",
CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ],
}];
[self waitForExpectations:@[ advertisementFoundExpectation ] timeout:3];
}
- (void)testAlreadyScanning {
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Start scanning."];
[medium startScanningForService:[CBUUID UUIDWithString:kServiceUUID]
advertisementFoundHandler:^(id<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *data) {
}
completionHandler:nil];
[medium startScanningForService:[CBUUID UUIDWithString:kServiceUUID]
advertisementFoundHandler:^(id<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *data) {
}
completionHandler:^(NSError *error) {
XCTAssertNotNil(error);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testStartStopStartScanning {
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Start scanning."];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID];
[medium startScanningForService:serviceUUID
advertisementFoundHandler:^(id<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *data) {
}
completionHandler:^(NSError *error) {
XCTAssertNil(error);
[medium stopScanningWithCompletionHandler:^(NSError *error) {
XCTAssertNil(error);
[medium startScanningForService:serviceUUID
advertisementFoundHandler:^(id<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *data) {
}
completionHandler:^(NSError *error) {
XCTAssertNil(error);
[expectation fulfill];
}];
}];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
#pragma mark - Decode Advertisement Data
- (void)testDecodeAndroidStyleAdvertisementData {
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID];
NSDictionary<CBUUID *, NSData *> *expected = @{
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
};
NSDictionary<NSString *, id> *data = @{
CBAdvertisementDataServiceDataKey : @{
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
},
};
NSDictionary<CBUUID *, NSData *> *actual = [medium decodeAdvertisementData:data];
XCTAssertEqualObjects(expected, actual);
}
- (void)testDecodeAndroidStyleAdvertisementDataWithLocalName {
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID];
NSDictionary<CBUUID *, NSData *> *expected = @{
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
};
NSDictionary<NSString *, id> *data = @{
CBAdvertisementDataLocalNameKey : @"Nearby", // Just happens to be base64 decodable.
CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ],
CBAdvertisementDataServiceDataKey : @{
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
},
};
NSDictionary<CBUUID *, NSData *> *actual = [medium decodeAdvertisementData:data];
XCTAssertEqualObjects(expected, actual);
}
- (void)testDecodeAppleStyleAdvertisementData {
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID];
NSDictionary<CBUUID *, NSData *> *expected = @{
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
};
NSDictionary<NSString *, id> *data = @{
CBAdvertisementDataLocalNameKey : @"dGVzdA",
CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ],
};
NSDictionary<CBUUID *, NSData *> *actual = [medium decodeAdvertisementData:data];
XCTAssertEqualObjects(expected, actual);
}
- (void)testDecodeInvalidAdvertisementData {
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID];
NSDictionary<NSString *, id> *data = @{
CBAdvertisementDataLocalNameKey : @"!@#$",
CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ],
};
NSDictionary<CBUUID *, NSData *> *actual = [medium decodeAdvertisementData:data];
XCTAssertEqualObjects(@{}, actual);
}
- (void)testDecodeEmptyAdvertisementData {
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
NSDictionary<CBUUID *, NSData *> *actual = [medium decodeAdvertisementData:@{}];
XCTAssertEqualObjects(@{}, actual);
}
#pragma mark - Start GATT Server
- (void)testStartGATTServer {
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Start GATT server."];
[medium startGATTServerWithCompletionHandler:^(GNCBLEGATTServer *server, NSError *error) {
XCTAssertNotNil(server);
XCTAssertNil(error);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
#pragma mark - Start Advertising
- (void)testStartAdvertising {
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Start advertising."];
// Start advertising is fully covered with @c GNCBLEGATTServer tests. We are passing invalid
// advertising data here so we can test code paths relevant to @c GNCBLEMedium, but bail early
// enough to avoid making actual CoreBluetooth calls.
[medium startAdvertisingData:@{}
completionHandler:^(NSError *error) {
XCTAssertNotNil(error);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testStopAdvertising {
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"Stop advertising."];
// Stop advertising is fully covered with @c GNCBLEGATTServer tests. We are only testing stopping
// without having started which tests the code paths relevant to @c GNCBLEMedium.
[medium stopAdvertisingWithCompletionHandler:^(NSError *error) {
XCTAssertNil(error);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
#pragma mark - Connect
- (void)testSuccessfulConnect {
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Connect."];
[medium connectToGATTServerForPeripheral:[[GNCFakePeripheral alloc] init]
disconnectionHandler:nil
completionHandler:^(GNCBLEGATTClient *client, NSError *error) {
XCTAssertNotNil(client);
XCTAssertNil(error);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testFailedConnect {
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Connect."];
fakeCentralManager.didFailToConnectPeripheralError = [NSError errorWithDomain:@"fake"
code:0
userInfo:nil];
[medium connectToGATTServerForPeripheral:[[GNCFakePeripheral alloc] init]
disconnectionHandler:nil
completionHandler:^(GNCBLEGATTClient *client, NSError *error) {
XCTAssertNil(client);
XCTAssertNotNil(error);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:3];
}
- (void)testDisconnect {
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
XCTestExpectation *connectExpectation =
[[XCTestExpectation alloc] initWithDescription:@"Connect."];
XCTestExpectation *disconnectExpectation =
[[XCTestExpectation alloc] initWithDescription:@"Disconnect."];
GNCFakePeripheral *peripheral = [[GNCFakePeripheral alloc] init];
[medium connectToGATTServerForPeripheral:peripheral
disconnectionHandler:^() {
[disconnectExpectation fulfill];
}
completionHandler:^(GNCBLEGATTClient *client, NSError *error) {
XCTAssertNotNil(client);
XCTAssertNil(error);
[connectExpectation fulfill];
}];
[self waitForExpectations:@[ connectExpectation ] timeout:3];
[fakeCentralManager simulateCentralManagerDidDisconnectPeripheral:peripheral];
[self waitForExpectations:@[ disconnectExpectation ] timeout:3];
}
@end
@@ -13,6 +13,7 @@
// limitations under the License.
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h"
@@ -25,6 +26,7 @@ using Property = ::nearby::api::ble_v2::GattCharacteristic::Property;
using Permission = ::nearby::api::ble_v2::GattCharacteristic::Permission;
using WriteType = ::nearby::api::ble_v2::GattClient::WriteType;
using Uuid = ::nearby::Uuid;
using ByteArray = ::nearby::ByteArray;
@interface GNCBLEUtilsTest : XCTestCase
@end
@@ -120,6 +122,22 @@ using Uuid = ::nearby::Uuid;
XCTAssertEqual(actual.properties, properties);
}
- (void)testObjCServiceDataFromCpp {
CBUUID *serviceUUID1 = [CBUUID UUIDWithString:@"0000FEF3-0000-1000-8000-00805F9B34FB"];
CBUUID *serviceUUID2 = [CBUUID UUIDWithString:@"0000FEF4-0000-1000-8000-00805F9B34FB"];
NSData *data1 = [@"one" dataUsingEncoding:NSUTF8StringEncoding];
NSData *data2 = [@"two" dataUsingEncoding:NSUTF8StringEncoding];
absl::flat_hash_map<Uuid, ByteArray> service_data;
service_data[Uuid(0x0000FEF300001000, 0x800000805F9B34FB)] = ByteArray("one");
service_data[Uuid(0x0000FEF400001000, 0x800000805F9B34FB)] = ByteArray("two");
NSMutableDictionary<CBUUID *, NSData *> *actual =
::nearby::apple::ObjCServiceDataFromCPP(service_data);
XCTAssertEqualObjects(actual[serviceUUID1], data1);
XCTAssertEqualObjects(actual[serviceUUID2], data2);
}
#pragma mark - Objective-C to C++
- (void)testCPPUUIDFromObjC16Bit {
@@ -1,112 +0,0 @@
// Copyright 2022 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 <XCTest/XCTest.h>
#include "internal/platform/implementation/apple/bluetooth_adapter.h"
#include <memory>
#include <string>
#include <utility>
#include "internal/platform/byte_array.h"
#include "internal/platform/implementation/ble_v2.h"
#include "internal/platform/implementation/bluetooth_adapter.h"
#include "internal/platform/implementation/platform.h"
using ::nearby::ByteArray;
using ::nearby::Uuid;
using ::nearby::api::BluetoothAdapter;
using ::nearby::api::ImplementationPlatform;
using ::nearby::api::ble_v2::BleAdvertisementData;
using ::nearby::api::ble_v2::BleMedium;
using ::nearby::api::ble_v2::GattCharacteristic;
using ::nearby::api::ble_v2::TxPowerLevel;
using IOSBluetoothAdapter = ::nearby::apple::BluetoothAdapter;
static const char *const kAdvertisementString = "\x0a\x0b\x0c\x0d";
static const TxPowerLevel kTxPowerLevel = TxPowerLevel::kHigh;
@interface GNCBleTest : XCTestCase
@end
// TODO(b/222392304): More tests on GNCBleTest.
@implementation GNCBleTest {
std::unique_ptr<BluetoothAdapter> _adapter;
std::unique_ptr<BleMedium> _ble;
}
- (void)setUp {
[super setUp];
_adapter = ImplementationPlatform::CreateBluetoothAdapter();
_ble = ImplementationPlatform::CreateBleV2Medium(*_adapter);
}
- (void)testStartandStopAdvertising {
Uuid service_uuid(1234, 5678);
ByteArray advertisement_bytes{std::string(kAdvertisementString)};
// Assemble regular advertisement data.
BleAdvertisementData advertising_data;
advertising_data.is_extended_advertisement = false;
advertising_data.service_data = {{service_uuid, advertisement_bytes}};
XCTAssertTrue(_ble->StartAdvertising(advertising_data,
{.tx_power_level = kTxPowerLevel, .is_connectable = true}));
[NSThread sleepForTimeInterval:0.1];
XCTAssertTrue(_ble->StopAdvertising());
}
- (void)testStartandStopScanning {
Uuid service_uuid(1234, 5678);
XCTAssertTrue(_ble->StartScanning(service_uuid, kTxPowerLevel, BleMedium::ScanCallback{}));
[NSThread sleepForTimeInterval:0.1];
XCTAssertTrue(_ble->StopScanning());
}
- (void)testGattServerWorking {
// Test creating gatt_server.
auto gatt_server = _ble->StartGattServer(/*ServerGattConnectionCallback=*/{});
XCTAssert(gatt_server != nullptr);
// Test creating characteristic.
Uuid service_uuid(1234, 5678);
Uuid characteristic_uuid(5678, 1234);
GattCharacteristic::Permission permission = GattCharacteristic::Permission::kRead;
GattCharacteristic::Property property = GattCharacteristic::Property::kRead;
// NOLINTNEXTLINE
absl::optional<GattCharacteristic> gatt_characteristic =
gatt_server->CreateCharacteristic(service_uuid, characteristic_uuid, permission, property);
XCTAssertTrue(gatt_characteristic.has_value());
// Test updating characteristic.
ByteArray any_byte("any");
XCTAssertTrue(gatt_server->UpdateCharacteristic(gatt_characteristic.value(), any_byte));
gatt_server->Stop();
}
- (void)testCreateGattClient {
IOSBluetoothAdapter *adapter = static_cast<IOSBluetoothAdapter *>(_adapter.get());
auto gatt_client = _ble->ConnectToGattServer(adapter->GetPeripheral(), kTxPowerLevel, {});
XCTAssert(gatt_client != nullptr);
}
@end
@@ -1,87 +0,0 @@
// Copyright 2022 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 <XCTest/XCTest.h>
#include <string>
#include "internal/platform/implementation/apple/bluetooth_adapter.h"
#include "internal/platform/implementation/ble_v2.h"
#include "internal/platform/implementation/bluetooth_adapter.h"
using ::nearby::apple::BlePeripheral;
using ::nearby::apple::BluetoothAdapter;
using ScanMode = ::nearby::api::BluetoothAdapter::ScanMode;
using Status = ::nearby::api::BluetoothAdapter::Status;
static const char kAdapterName[] = "MyBtAdapter";
static const char kMacAddress[] = "4C:8B:1D:CE:BA:D1";
@interface GNCBluetoothAdapterTest : XCTestCase
@end
@implementation GNCBluetoothAdapterTest
- (void)testName {
BluetoothAdapter adapter;
XCTAssertTrue(adapter.SetName(kAdapterName));
XCTAssertEqual(adapter.GetName(), std::string(kAdapterName));
}
- (void)testGetScanMode {
BluetoothAdapter adapter;
// Always return kNone as ScanMode is not supported .
XCTAssertEqual(adapter.GetScanMode(), ScanMode::kNone);
}
- (void)testSetScanMode_DefaultUnsupported {
BluetoothAdapter adapter;
XCTAssertFalse(adapter.SetScanMode(ScanMode::kNone));
XCTAssertFalse(adapter.SetScanMode(ScanMode::kConnectable));
XCTAssertFalse(adapter.SetScanMode(ScanMode::kConnectableDiscoverable));
}
- (void)testSetStatus {
BluetoothAdapter adapter;
XCTAssertTrue(adapter.SetStatus(Status::kDisabled));
XCTAssertFalse(adapter.IsEnabled());
XCTAssertTrue(adapter.SetStatus(Status::kEnabled));
XCTAssertTrue(adapter.IsEnabled());
}
- (void)testMacAddress {
BluetoothAdapter adapter;
adapter.SetMacAddress(kMacAddress);
XCTAssertEqual(adapter.GetMacAddress(), std::string(kMacAddress));
}
- (void)testGetPeripheral {
BluetoothAdapter adapter;
adapter.SetMacAddress(kMacAddress);
// The peripheral from adapter, the MAC address is the same.
BlePeripheral& peripheral = adapter.GetPeripheral();
XCTAssertEqual(adapter.GetMacAddress(), peripheral.GetAddress());
}
@end
@@ -0,0 +1,70 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCCentralManager.h"
NS_ASSUME_NONNULL_BEGIN
/** A fake implementation of @c GNCCentralManager to inject for testing. */
@interface GNCFakeCentralManager : NSObject <GNCCentralManager>
/** The list of services being scanned for. */
@property(nonatomic, nullable, readonly) NSArray<CBUUID *> *serviceUUIDs;
/**
* Similates a @c connectPeripheral:options: error.
*
* Setting this error to a value other than @c nil will simulate a failure when calling
* @c connectPeripheral:options: and will call the
* @c gnc_centralManager:didFailToConnectPeripheral:error: delegate method with the provided error.
*/
@property(nonatomic, nullable, readwrite) NSError *didFailToConnectPeripheralError;
/**
* Simulates a state update event.
*
* Updates the central manager state to the provided value and calls the
* @c gnc_centralManagerDidUpdateState: delegate method.
*
* @param fakeState The new state to transition to.
*/
- (void)simulateCentralManagerDidUpdateState:(CBManagerState)fakeState;
/**
* Simulates a peripheral discovery event.
*
* Calls the @c gnc_centralManager:didDiscoverPeripheral:advertisementData:RSSI: delegate method.
*
* @param peripheral The discovered peripheral.
* @param peripheral A dictionary containing any advertisement data.
*/
- (void)simulateCentralManagerDidDiscoverPeripheral:(id<GNCPeripheral>)peripheral
advertisementData:
(NSDictionary<NSString *, id> *)advertisementData;
/**
* Simulates a peripheral disconnection event.
*
* Calls the @c gnc_centralManager:didDisconnectPeripheral:error: delegate method.
*
* @param peripheral The now-disconnected peripheral.
*/
- (void)simulateCentralManagerDidDisconnectPeripheral:(id<GNCPeripheral>)peripheral;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,86 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "internal/platform/implementation/apple/Tests/GNCFakeCentralManager.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCCentralManager.h"
#import "internal/platform/implementation/apple/Tests/GNCFakePeripheral.h"
@implementation GNCFakeCentralManager {
CBManagerState _state;
NSArray<CBUUID *> *_serviceUUIDs;
}
@synthesize centralDelegate;
- (instancetype)init {
self = [super init];
if (self) {
_state = CBManagerStateUnknown;
}
return self;
}
- (CBManagerState)state {
return _state;
}
- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs
options:(nullable NSDictionary<NSString *, id> *)options {
_serviceUUIDs = serviceUUIDs;
}
- (void)connectPeripheral:(id<GNCPeripheral>)peripheral
options:(nullable NSDictionary<NSString *, id> *)options {
if (_didFailToConnectPeripheralError) {
[centralDelegate gnc_centralManager:self
didFailToConnectPeripheral:peripheral
error:_didFailToConnectPeripheralError];
return;
}
[centralDelegate gnc_centralManager:self didConnectPeripheral:peripheral];
}
- (void)stopScan {
}
#pragma mark - Testing Helpers
- (NSArray<CBUUID *> *)serviceUUIDs {
return _serviceUUIDs;
}
- (void)simulateCentralManagerDidUpdateState:(CBManagerState)fakeState {
_state = fakeState;
[centralDelegate gnc_centralManagerDidUpdateState:self];
}
- (void)simulateCentralManagerDidDiscoverPeripheral:(id<GNCPeripheral>)peripheral
advertisementData:
(NSDictionary<NSString *, id> *)advertisementData {
[centralDelegate gnc_centralManager:self
didDiscoverPeripheral:peripheral
advertisementData:advertisementData
RSSI:[NSNumber numberWithInt:0]];
}
- (void)simulateCentralManagerDidDisconnectPeripheral:(id<GNCPeripheral>)peripheral {
[centralDelegate gnc_centralManager:self didDisconnectPeripheral:peripheral error:nil];
}
@end
@@ -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 <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#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 <GNCPeripheral>
/**
* 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
@@ -0,0 +1,124 @@
// 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 <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#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<CBCharacteristic *> *characteristics;
@end
@interface CBCharacteristic ()
// Change property to readwrite for tests.
@property(retain, readwrite, nullable) NSData *value;
@end
@implementation GNCFakePeripheral {
NSMutableArray<CBService *> *_services;
NSUUID *_identifier;
}
@synthesize peripheralDelegate;
- (instancetype)init {
self = [super init];
if (self) {
_services = [[NSMutableArray alloc] init];
_identifier = [[NSUUID alloc] init];
}
return self;
}
- (NSUUID *)identifier {
return _identifier;
}
- (nullable NSArray<CBService *> *)services {
return _services;
}
- (void)discoverServices:(nullable NSArray<CBUUID *> *)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<CBUUID *> *)characteristicUUIDs
forService:(CBService *)service {
[self delayDelegateUsingBlock:^() {
if (!_discoverCharacteristicsForServiceError) {
NSMutableArray<CBCharacteristic *> *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
@@ -61,9 +61,6 @@
BOOL _isAdvertising;
NSDictionary<NSString *, id> *_advertisementData;
NSMutableArray<CBService *> *_services;
// Used to deliver delegate callbacks.
dispatch_queue_t _queue;
}
@synthesize peripheralDelegate;
@@ -79,7 +76,6 @@
_state = CBManagerStateUnknown;
_advertisementData = nil;
_services = [[NSMutableArray alloc] init];
_queue = dispatch_queue_create("com.nearby.GNCFakePeripheralManager", DISPATCH_QUEUE_SERIAL);
}
return self;
}
@@ -96,9 +92,7 @@
if (!_didAddServiceError) {
[_services addObject:service];
}
dispatch_async(_queue, ^{
[peripheralDelegate gnc_peripheralManager:self didAddService:service error:_didAddServiceError];
});
[peripheralDelegate gnc_peripheralManager:self didAddService:service error:_didAddServiceError];
}
- (void)removeService:(CBMutableService *)service {
@@ -112,10 +106,8 @@
- (void)startAdvertising:(NSDictionary<NSString *, id> *)advertisementData {
_isAdvertising = _didStartAdvertisingError == nil;
_advertisementData = advertisementData;
dispatch_async(_queue, ^{
[peripheralDelegate gnc_peripheralManagerDidStartAdvertising:self
error:_didStartAdvertisingError];
});
[peripheralDelegate gnc_peripheralManagerDidStartAdvertising:self
error:_didStartAdvertisingError];
}
- (void)respondToRequest:(CBATTRequest *)request withResult:(CBATTError)result {
@@ -126,6 +118,10 @@
[_respondToRequestErrorExpectation fulfill];
}
- (void)stopAdvertising {
_isAdvertising = false;
}
#pragma mark - Testing Helpers
- (NSArray<CBService *> *)services {
@@ -138,18 +134,14 @@
- (void)simulatePeripheralManagerDidUpdateState:(CBManagerState)fakeState {
_state = fakeState;
dispatch_async(_queue, ^{
[peripheralDelegate gnc_peripheralManagerDidUpdateState:self];
});
[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];
});
[peripheralDelegate gnc_peripheralManager:self didReceiveReadRequest:request];
}
@end
@@ -0,0 +1,83 @@
// 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.
#include "internal/platform/implementation/platform.h"
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
void GNCEnsureFileAtPath(std::string path) {
[NSFileManager.defaultManager
createDirectoryAtPath:[@(path.c_str()) stringByDeletingLastPathComponent]
withIntermediateDirectories:YES
attributes:nil
error:nil];
[[NSData data] writeToFile:@(path.c_str()) options:0 error:nil];
}
@interface GNCPlatformTest : XCTestCase
@end
@implementation GNCPlatformTest
- (void)testGetCustomSavePath {
NSString *expected = [NSURL fileURLWithPath:@"a/b/c.d"].path;
std::string actual = nearby::api::ImplementationPlatform::GetCustomSavePath("a/b", "c.d");
XCTAssertEqualObjects(@(actual.c_str()), expected);
}
- (void)testGetCustomSavePathWithIllegalCharacters {
NSString *expected = [NSURL fileURLWithPath:@"a/b/Fi?le*: Name.ext"].path;
std::string actual =
nearby::api::ImplementationPlatform::GetCustomSavePath("a/b", "Fi?le*/ Name.ext");
XCTAssertEqualObjects(@(actual.c_str()), expected);
}
- (void)testGetCustomSavePathWithPathEscapingCharacters {
NSString *expected = [NSURL fileURLWithPath:@"a/b/..:c:..:d.e"].path;
std::string actual =
nearby::api::ImplementationPlatform::GetCustomSavePath("a/../../b", "../c/../d.e");
XCTAssertEqualObjects(@(actual.c_str()), expected);
}
- (void)testGetCustomSavePathDuplicateNames {
NSString *expected1 = [NSURL fileURLWithPath:@"a/b/cat.jpg"].path;
NSString *expected2 = [NSURL fileURLWithPath:@"a/b/cat 2.jpg"].path;
NSString *expected3 = [NSURL fileURLWithPath:@"a/b/cat 3.jpg"].path;
std::string actual1 = nearby::api::ImplementationPlatform::GetCustomSavePath("a/b", "cat.jpg");
GNCEnsureFileAtPath(actual1);
std::string actual2 = nearby::api::ImplementationPlatform::GetCustomSavePath("a/b", "cat.jpg");
GNCEnsureFileAtPath(actual2);
std::string actual3 = nearby::api::ImplementationPlatform::GetCustomSavePath("a/b", "cat.jpg");
// Cleanup created files.
[NSFileManager.defaultManager removeItemAtPath:@(actual1.c_str()) error:nil];
[NSFileManager.defaultManager removeItemAtPath:@(actual2.c_str()) error:nil];
XCTAssertEqualObjects(@(actual1.c_str()), expected1);
XCTAssertEqualObjects(@(actual2.c_str()), expected2);
XCTAssertEqualObjects(@(actual3.c_str()), expected3);
}
- (void)testGetDownloadPath {
NSString *expected =
[[NSURL fileURLWithPath:NSTemporaryDirectory()] URLByAppendingPathComponent:@"a/b/c.d"].path;
std::string actual = nearby::api::ImplementationPlatform::GetDownloadPath("a/b", "c.d");
XCTAssertEqualObjects(@(actual.c_str()), expected);
}
@end
@@ -25,7 +25,7 @@
- (void)testEncodingWithPadding {
NSString *expected = @"AQ";
NSData *data = [[NSData alloc] initWithBase64EncodedString:@"AQ==" options:0];
NSString *actual = [data webSafebase64EncodedString];
NSString *actual = [data webSafeBase64EncodedString];
XCTAssertEqualObjects(expected, actual);
}
@@ -35,8 +35,58 @@
[[NSData alloc] initWithBase64EncodedString:
@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
options:0];
NSString *actual = [data webSafebase64EncodedString];
NSString *actual = [data webSafeBase64EncodedString];
XCTAssertEqualObjects(expected, actual);
}
- (void)testDecodingWithoutPadding {
NSData *expected = [[NSData alloc] initWithBase64EncodedString:@"AQ==" options:0];
NSData *actual = [[NSData alloc] initWithWebSafeBase64EncodedString:@"AQ"];
XCTAssertNotNil(actual);
XCTAssertEqualObjects(expected, actual);
}
- (void)testDecodingNoPad {
NSData *expected = [[NSData alloc] initWithBase64EncodedString:@"aaaa" options:0];
NSData *actual = [[NSData alloc] initWithWebSafeBase64EncodedString:@"aaaa"];
XCTAssertNotNil(actual);
XCTAssertEqualObjects(expected, actual);
}
- (void)testDecoding1Pad {
NSData *expected = [[NSData alloc] initWithBase64EncodedString:@"aaa=" options:0];
NSData *actual = [[NSData alloc] initWithWebSafeBase64EncodedString:@"aaa"];
XCTAssertNotNil(actual);
XCTAssertEqualObjects(expected, actual);
}
- (void)testDecoding2Pad {
NSData *expected = [[NSData alloc] initWithBase64EncodedString:@"aa==" options:0];
NSData *actual = [[NSData alloc] initWithWebSafeBase64EncodedString:@"aa"];
XCTAssertNotNil(actual);
XCTAssertEqualObjects(expected, actual);
}
- (void)testDecodingSingleCharacterInLastQuadruple {
NSData *actual = [[NSData alloc] initWithWebSafeBase64EncodedString:@"aaaab"];
XCTAssertNil(actual);
}
- (void)testDecodingWithAllValidCharacters {
NSData *expected =
[[NSData alloc] initWithBase64EncodedString:
@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
options:0];
NSData *actual =
[[NSData alloc] initWithWebSafeBase64EncodedString:
@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"];
XCTAssertNotNil(actual);
XCTAssertEqualObjects(expected, actual);
}
- (void)testDecodingWithIllegalCharacters {
NSData *actual = [[NSData alloc] initWithWebSafeBase64EncodedString:@"@#$^&*()"];
XCTAssertNil(actual);
}
@end
+7 -212
View File
@@ -12,218 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_APPLE_BLE_H_
#define THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_APPLE_BLE_H_
// Note: File language is detected using heuristics. Many Objective-C++ headers are incorrectly
// classified as C++ resulting in invalid linter errors. The use of "NSArray" and other Foundation
// classes like "NSData", "NSDictionary" and "NSUUID" are highly weighted for Objective-C and
// Objective-C++ scores. Oddly, "#import <Foundation/Foundation.h>" does not contribute any points.
// This comment alone should be enough to trick the IDE in to believing this is actually some sort
// of Objective-C file. See: cs/google3/devtools/search/lang/recognize_language_classifiers_data
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#include <memory>
#include <string>
#include <vector>
#include "absl/types/optional.h"
#include "internal/platform/cancellation_flag.h"
#include "internal/platform/implementation/apple/bluetooth_adapter.h"
#include "internal/platform/implementation/ble_v2.h"
#include "internal/platform/implementation/bluetooth_adapter.h"
#import "internal/platform/implementation/apple/Mediums/GNCMConnection.h"
@class GNCMBlePeripheral, GNCMBleCentral;
namespace nearby {
namespace apple {
/** InputStream that reads from GNCMConnection. */
class BleInputStream : public InputStream {
public:
BleInputStream();
~BleInputStream() override;
ExceptionOr<ByteArray> Read(std::int64_t size) override;
Exception Close() override;
GNCMConnectionHandlers *GetConnectionHandlers() { return connectionHandlers_; }
private:
GNCMConnectionHandlers *connectionHandlers_;
NSMutableArray<NSData *> *newDataPackets_;
NSMutableData *accumulatedData_;
NSCondition *condition_;
};
/** OutputStream that writes to GNCMConnection. */
class BleOutputStream : public OutputStream {
public:
explicit BleOutputStream(id<GNCMConnection> connection)
: connection_(connection), condition_([[NSCondition alloc] init]) {}
~BleOutputStream() override;
Exception Write(const ByteArray &data) override;
Exception Flush() override;
Exception Close() override;
private:
id<GNCMConnection> connection_;
NSCondition *condition_;
};
/** Concrete BleSocket implementation. */
class BleSocket : public api::ble_v2::BleSocket {
public:
BleSocket(id<GNCMConnection> connection, BlePeripheral *peripheral);
~BleSocket() override;
InputStream &GetInputStream() override { return *input_stream_; }
OutputStream &GetOutputStream() override { return *output_stream_; }
Exception Close() override ABSL_LOCKS_EXCLUDED(mutex_);
BlePeripheral *GetRemotePeripheral() override { return peripheral_; }
bool IsClosed() const ABSL_LOCKS_EXCLUDED(mutex_);
private:
void DoClose() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
mutable absl::Mutex mutex_;
bool closed_ ABSL_GUARDED_BY(mutex_) = false;
std::unique_ptr<BleInputStream> input_stream_;
std::unique_ptr<BleOutputStream> output_stream_;
BlePeripheral *peripheral_;
};
/** Concrete BleServerSocket implementation. */
class BleServerSocket : public api::ble_v2::BleServerSocket {
public:
~BleServerSocket() override;
std::unique_ptr<api::ble_v2::BleSocket> Accept() override ABSL_LOCKS_EXCLUDED(mutex_);
Exception Close() override ABSL_LOCKS_EXCLUDED(mutex_);
bool Connect(std::unique_ptr<BleSocket> socket) ABSL_LOCKS_EXCLUDED(mutex_);
void SetCloseNotifier(absl::AnyInvocable<void()> notifier) ABSL_LOCKS_EXCLUDED(mutex_);
private:
Exception DoClose() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
mutable absl::Mutex mutex_;
absl::CondVar cond_;
absl::flat_hash_set<std::unique_ptr<BleSocket>> pending_sockets_ ABSL_GUARDED_BY(mutex_);
absl::AnyInvocable<void()> close_notifier_ ABSL_GUARDED_BY(mutex_);
bool closed_ ABSL_GUARDED_BY(mutex_) = false;
};
/** Concrete BleMedium implementation. */
class BleMedium : public api::ble_v2::BleMedium {
public:
explicit BleMedium(api::BluetoothAdapter &adapter);
// api::BleMedium:
bool StartAdvertising(const api::ble_v2::BleAdvertisementData &advertising_data,
api::ble_v2::AdvertiseParameters advertise_set_parameters) override;
bool StopAdvertising() override;
std::unique_ptr<AdvertisingSession> StartAdvertising(
const api::ble_v2::BleAdvertisementData &advertising_data,
api::ble_v2::AdvertiseParameters advertise_parameters, AdvertisingCallback callback) override;
bool StartScanning(const Uuid &service_uuid, api::ble_v2::TxPowerLevel tx_power_level,
api::ble_v2::BleMedium::ScanCallback scan_callback) override;
bool StopScanning() override;
std::unique_ptr<ScanningSession> StartScanning(const Uuid &service_uuid,
api::ble_v2::TxPowerLevel tx_power_level,
ScanningCallback callback) override;
std::unique_ptr<api::ble_v2::GattServer> StartGattServer(
api::ble_v2::ServerGattConnectionCallback callback) override;
std::unique_ptr<api::ble_v2::GattClient> ConnectToGattServer(
api::ble_v2::BlePeripheral &peripheral, api::ble_v2::TxPowerLevel tx_power_level,
api::ble_v2::ClientGattConnectionCallback callback) override;
std::unique_ptr<api::ble_v2::BleServerSocket> OpenServerSocket(
const std::string &service_id) override;
std::unique_ptr<api::ble_v2::BleSocket> Connect(const std::string &service_id,
api::ble_v2::TxPowerLevel tx_power_level,
api::ble_v2::BlePeripheral &peripheral,
CancellationFlag *cancellation_flag) override;
bool IsExtendedAdvertisementsAvailable() override;
bool GetRemotePeripheral(const std::string& mac_address,
GetRemotePeripheralCallback callback) override {
return false;
}
bool GetRemotePeripheral(api::ble_v2::BlePeripheral::UniqueId id,
GetRemotePeripheralCallback callback) override {
return false;
}
private:
// A concrete implemenation for GattServer.
class GattServer : public api::ble_v2::GattServer {
public:
GattServer() = default;
GattServer(BluetoothAdapter *adapter, GNCMBlePeripheral *peripheral)
: adapter_(adapter), peripheral_(peripheral) {}
absl::optional<api::ble_v2::GattCharacteristic> CreateCharacteristic(
const Uuid &service_uuid, const Uuid &characteristic_uuid,
api::ble_v2::GattCharacteristic::Permission permission,
api::ble_v2::GattCharacteristic::Property property) override;
bool UpdateCharacteristic(const api::ble_v2::GattCharacteristic &characteristic,
const nearby::ByteArray &value) override;
absl::Status NotifyCharacteristicChanged(const api::ble_v2::GattCharacteristic &characteristic,
bool confirm, const ByteArray &new_value) override;
void Stop() override;
BlePeripheral &GetBlePeripheral() override { return adapter_->GetPeripheral(); }
private:
BluetoothAdapter *adapter_ = nullptr;
GNCMBlePeripheral *peripheral_;
};
// A concrete implemenation for GattClient.
class GattClient : public api::ble_v2::GattClient {
public:
GattClient() = default;
explicit GattClient(GNCMBleCentral *central, const std::string &peripheral_id)
: central_(central), peripheral_id_(peripheral_id) {}
bool DiscoverServiceAndCharacteristics(const Uuid &service_uuid,
const std::vector<Uuid> &characteristic_uuids) override;
// NOLINTNEXTLINE
absl::optional<api::ble_v2::GattCharacteristic> GetCharacteristic(
const Uuid &service_uuid, const Uuid &characteristic_uuid) override;
// NOLINTNEXTLINE
absl::optional<std::string> ReadCharacteristic(
const api::ble_v2::GattCharacteristic &characteristic) override;
bool WriteCharacteristic(const api::ble_v2::GattCharacteristic &characteristic,
absl::string_view value,
api::ble_v2::GattClient::WriteType write_type) override;
bool SetCharacteristicSubscription(
const api::ble_v2::GattCharacteristic &characteristic, bool enable,
absl::AnyInvocable<void(absl::string_view value)> on_characteristic_changed_cb) override;
void Disconnect() override;
private:
GNCMBleCentral *central_;
std::string peripheral_id_;
absl::flat_hash_map<api::ble_v2::GattCharacteristic, const std::string>
gatt_characteristic_values_;
};
absl::Mutex mutex_;
BluetoothAdapter *adapter_;
GNCMBlePeripheral *peripheral_;
GNCMBleCentral *central_;
absl::flat_hash_map<std::string, BleServerSocket *> server_sockets_ ABSL_GUARDED_BY(mutex_);
dispatch_queue_t callback_queue_;
};
} // namespace apple
} // namespace nearby
#endif // THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_APPLE_BLE_H_
#import "internal/platform/implementation/apple/ble_medium.h"
@@ -1,600 +0,0 @@
// Copyright 2022 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/ble.h"
#include <CoreBluetooth/CoreBluetooth.h>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "internal/platform/implementation/apple/bluetooth_adapter.h"
#include "internal/platform/implementation/apple/utils.h"
#include "internal/platform/implementation/ble_v2.h"
#import "internal/platform/implementation/apple/Mediums/Ble/GNCMBleCentral.h"
#import "internal/platform/implementation/apple/Mediums/Ble/GNCMBlePeripheral.h"
namespace nearby {
namespace apple {
using Permission = api::ble_v2::GattCharacteristic::Permission;
using Property = api::ble_v2::GattCharacteristic::Property;
namespace {
CBAttributePermissions PermissionToCBPermissions(Permission permission) {
CBAttributePermissions characteristPermissions = 0;
if ((permission & Permission::kRead) != Permission::kNone) {
characteristPermissions |= CBAttributePermissionsReadable;
}
if ((permission & Permission::kWrite) != Permission::kNone) {
characteristPermissions |= CBAttributePermissionsWriteable;
}
return characteristPermissions;
}
CBCharacteristicProperties PropertiesToCBProperties(Property property) {
CBCharacteristicProperties characteristicProperties = 0;
if ((property & Property::kRead) != Property::kNone) {
characteristicProperties |= CBCharacteristicPropertyRead;
}
if ((property & Property::kWrite) != Property::kNone) {
characteristicProperties |= CBCharacteristicPropertyWrite;
}
if ((property & Property::kIndicate) != Property::kNone) {
characteristicProperties |= CBCharacteristicPropertyIndicate;
}
if ((property & Property::kNotify) != Property::kNone) {
characteristicProperties |= CBCharacteristicPropertyNotify;
}
return characteristicProperties;
}
} // namespace
using ::nearby::api::ble_v2::BleAdvertisementData;
using ::nearby::api::ble_v2::TxPowerLevel;
using ScanCallback = ::nearby::api::ble_v2::BleMedium::ScanCallback;
/** InputStream that reads from GNCMConnection. */
BleInputStream::BleInputStream()
: newDataPackets_([NSMutableArray array]),
accumulatedData_([NSMutableData data]),
condition_([[NSCondition alloc] init]) {
// Create the handlers of incoming data from the remote endpoint.
connectionHandlers_ = [GNCMConnectionHandlers
payloadHandler:^(NSData* data) {
[condition_ lock];
// Add the incoming data to the data packet array to be processed in read() below.
[newDataPackets_ addObject:data];
[condition_ signal];
[condition_ unlock];
}
disconnectedHandler:^{
[condition_ lock];
// Release the data packet array, meaning the stream has been closed or severed.
newDataPackets_ = nil;
[condition_ signal];
[condition_ unlock];
}];
}
BleInputStream::~BleInputStream() {
NSCAssert(!newDataPackets_, @"BleInputStream not closed before destruction");
}
ExceptionOr<ByteArray> BleInputStream::Read(std::int64_t size) {
// Block until either (a) the connection has been closed, (b) we have enough data to return.
NSData* dataToReturn;
[condition_ lock];
while (true) {
// Check if the stream has been closed or severed.
if (!newDataPackets_) break;
if (newDataPackets_.count > 0) {
// Add the packet data to the accumulated data.
for (NSData* data in newDataPackets_) {
if (data.length > 0) {
[accumulatedData_ appendData:data];
}
}
[newDataPackets_ removeAllObjects];
}
if ((size == -1) && (accumulatedData_.length > 0)) {
// Return all of the data.
dataToReturn = accumulatedData_;
accumulatedData_ = [NSMutableData data];
break;
} else if (accumulatedData_.length > 0) {
// Return up to |size| bytes of the data.
std::int64_t sizeToReturn = (accumulatedData_.length < size) ? accumulatedData_.length : size;
NSRange range = NSMakeRange(0, (NSUInteger)sizeToReturn);
dataToReturn = [accumulatedData_ subdataWithRange:range];
[accumulatedData_ replaceBytesInRange:range withBytes:nil length:0];
break;
}
[condition_ wait];
}
[condition_ unlock];
if (dataToReturn) {
NSLog(@"[NEARBY] Input stream: Received data of size: %lu", (unsigned long)dataToReturn.length);
return ExceptionOr<ByteArray>(ByteArrayFromNSData(dataToReturn));
} else {
return ExceptionOr<ByteArray>{Exception::kIo};
}
}
Exception BleInputStream::Close() {
// Unblock pending read operation.
[condition_ lock];
newDataPackets_ = nil;
[condition_ signal];
[condition_ unlock];
return {Exception::kSuccess};
}
/** OutputStream that writes to GNCMConnection. */
BleOutputStream::~BleOutputStream() {
NSCAssert(!connection_, @"BleOutputStream not closed before destruction");
}
Exception BleOutputStream::Write(const ByteArray& data) {
[condition_ lock];
NSLog(@"[NEARBY] Sending data of size: %lu", (unsigned long)NSDataFromByteArray(data).length);
NSMutableData* packet = [NSMutableData dataWithData:NSDataFromByteArray(data)];
// Send the data, blocking until the completion handler is called.
__block GNCMPayloadResult sendResult = GNCMPayloadResultFailure;
__block bool isComplete = NO;
NSCondition* condition = condition_; // don't capture |this| in completion
// Check if connection_ is nil, then just don't wait and return as failure.
if (connection_ != nil) {
[connection_ sendData:packet
progressHandler:^(size_t count) {
}
completion:^(GNCMPayloadResult result) {
// Make sure we haven't already reported completion before. This prevents a crash
// where we try leaving a dispatch group more times than we entered it.
// b/79095653.
if (isComplete) {
return;
}
isComplete = YES;
sendResult = result;
[condition lock];
[condition signal];
[condition unlock];
}];
[condition_ wait];
[condition_ unlock];
} else {
sendResult = GNCMPayloadResultFailure;
[condition_ unlock];
}
if (sendResult == GNCMPayloadResultSuccess) {
return {Exception::kSuccess};
} else {
return {Exception::kIo};
}
}
Exception BleOutputStream::Flush() {
// The write() function blocks until the data is received by the remote endpoint, so there's
// nothing to do here.
return {Exception::kSuccess};
}
Exception BleOutputStream::Close() {
// Unblock pending write operation.
[condition_ lock];
connection_ = nil;
[condition_ signal];
[condition_ unlock];
return {Exception::kSuccess};
}
/** BleSocket implementation.*/
BleSocket::BleSocket(id<GNCMConnection> connection, BlePeripheral* peripheral)
: input_stream_(new BleInputStream()),
output_stream_(new BleOutputStream(connection)),
peripheral_(peripheral) {}
BleSocket::~BleSocket() {
absl::MutexLock lock(&mutex_);
DoClose();
}
bool BleSocket::IsClosed() const {
absl::MutexLock lock(&mutex_);
return closed_;
}
Exception BleSocket::Close() {
absl::MutexLock lock(&mutex_);
DoClose();
return {Exception::kSuccess};
}
void BleSocket::DoClose() {
if (!closed_) {
input_stream_->Close();
output_stream_->Close();
closed_ = true;
}
}
/** WifiLanServerSocket implementation. */
BleServerSocket::~BleServerSocket() {
absl::MutexLock lock(&mutex_);
DoClose();
}
std::unique_ptr<api::ble_v2::BleSocket> BleServerSocket::Accept() {
absl::MutexLock lock(&mutex_);
while (!closed_ && pending_sockets_.empty()) {
cond_.Wait(&mutex_);
}
// Return early if closed.
if (closed_) return {};
auto remote_socket = std::move(pending_sockets_.extract(pending_sockets_.begin()).value());
return std::move(remote_socket);
}
bool BleServerSocket::Connect(std::unique_ptr<BleSocket> socket) {
absl::MutexLock lock(&mutex_);
if (closed_) {
return false;
}
// add client socket to the pending list
pending_sockets_.insert(std::move(socket));
cond_.SignalAll();
if (closed_) {
return false;
}
return true;
}
void BleServerSocket::SetCloseNotifier(absl::AnyInvocable<void()> notifier) {
absl::MutexLock lock(&mutex_);
close_notifier_ = std::move(notifier);
}
Exception BleServerSocket::Close() {
absl::MutexLock lock(&mutex_);
return DoClose();
}
Exception BleServerSocket::DoClose() {
bool should_notify = !closed_;
closed_ = true;
if (should_notify) {
cond_.SignalAll();
if (close_notifier_) {
auto notifier = std::move(close_notifier_);
mutex_.Unlock();
// Notifier may contain calls to public API, and may cause deadlock, if
// mutex_ is held during the call.
notifier();
mutex_.Lock();
}
}
return {Exception::kSuccess};
}
/** BleMedium implementation. */
BleMedium::BleMedium(::nearby::api::BluetoothAdapter& adapter)
: adapter_(static_cast<BluetoothAdapter*>(&adapter)) {}
bool BleMedium::StartAdvertising(
const BleAdvertisementData& advertising_data,
::nearby::api::ble_v2::AdvertiseParameters advertise_set_parameters) {
if (advertising_data.service_data.empty()) {
return false;
}
const auto& service_uuid = advertising_data.service_data.begin()->first.Get16BitAsString();
const ByteArray& service_data_bytes = advertising_data.service_data.begin()->second;
if (!peripheral_) {
peripheral_ = [[GNCMBlePeripheral alloc] init];
}
auto& peripheral = adapter_->GetPeripheral();
[peripheral_
startAdvertisingWithServiceUUID:ObjCStringFromCppString(service_uuid)
advertisementData:NSDataFromByteArray(service_data_bytes)
endpointConnectedHandler:^GNCMConnectionHandlers*(id<GNCMConnection> connection) {
// TODO(edwinwu): This server_socket is supposed to be gotten from the map by key of
// servcie_id. We now always get the first iteration since we don't know the key now.
// Try the way to move the Ble socket frame verification up to one layer.
std::string service_id;
BleServerSocket* server_socket;
if (!server_sockets_.empty()) {
service_id = server_sockets_.begin()->first;
server_socket = server_sockets_.begin()->second;
} else {
return nil;
}
auto socket = std::make_unique<BleSocket>(connection, &peripheral);
GNCMConnectionHandlers* connectionHandlers =
static_cast<BleInputStream&>(socket->GetInputStream()).GetConnectionHandlers();
server_socket->Connect(std::move(socket));
return connectionHandlers;
}
callbackQueue:callback_queue_];
return true;
}
bool BleMedium::StopAdvertising() {
peripheral_ = nil;
return true;
}
std::unique_ptr<BleMedium::AdvertisingSession> BleMedium::StartAdvertising(
const api::ble_v2::BleAdvertisementData& advertising_data,
api::ble_v2::AdvertiseParameters advertise_parameters,
BleMedium::AdvertisingCallback callback) {
// TODO(hais): add real impl for iOs StartAdvertising
return std::make_unique<AdvertisingSession>(AdvertisingSession{});
}
bool BleMedium::StartScanning(const Uuid& service_uuid, TxPowerLevel tx_power_level,
ScanCallback scan_callback) {
if (!central_) {
central_ = [[GNCMBleCentral alloc] init];
}
__block ScanCallback callback = std::move(scan_callback);
[central_ startScanningWithServiceUUID:ObjCStringFromCppString(service_uuid.Get16BitAsString())
scanResultHandler:^(NSString* peripheralID, NSData* serviceData) {
BleAdvertisementData advertisement_data;
advertisement_data.service_data = {{service_uuid, ByteArrayFromNSData(serviceData)}};
BlePeripheral& peripheral = adapter_->GetPeripheral();
peripheral.SetPeripheralId(CppStringFromObjCString(peripheralID));
callback.advertisement_found_cb(peripheral, advertisement_data);
}
requestConnectionHandler:^(GNCMBleConnectionRequester connectionRequester) {
BlePeripheral& peripheral = adapter_->GetPeripheral();
peripheral.SetConnectionRequester(connectionRequester);
}
callbackQueue:callback_queue_];
return true;
}
bool BleMedium::StopScanning() {
central_ = nil;
return true;
}
std::unique_ptr<BleMedium::ScanningSession> BleMedium::StartScanning(
const Uuid& service_uuid, TxPowerLevel tx_power_level, BleMedium::ScanningCallback callback) {
// TODO(hais): add real impl for windows StartScanning.
return std::make_unique<ScanningSession>(ScanningSession{});
}
std::unique_ptr<api::ble_v2::GattServer> BleMedium::StartGattServer(
api::ble_v2::ServerGattConnectionCallback callback) {
if (!peripheral_) {
peripheral_ = [[GNCMBlePeripheral alloc] init];
}
return std::make_unique<GattServer>(adapter_, peripheral_);
}
std::unique_ptr<api::ble_v2::GattClient> BleMedium::ConnectToGattServer(
api::ble_v2::BlePeripheral& peripheral, TxPowerLevel tx_power_level,
api::ble_v2::ClientGattConnectionCallback callback) {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSError* connectedError;
BlePeripheral iosPeripheral = static_cast<BlePeripheral&>(peripheral);
std::string peripheral_id = iosPeripheral.GetPeripheralId();
[central_ connectGattServerWithPeripheralID:ObjCStringFromCppString(peripheral_id)
gattConnectionResultHandler:^(NSError* _Nullable error) {
connectedError = error;
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC));
if (connectedError) {
return nullptr;
}
return std::make_unique<GattClient>(central_, peripheral_id);
}
std::unique_ptr<api::ble_v2::BleServerSocket> BleMedium::OpenServerSocket(
const std::string& service_id) {
auto server_socket = std::make_unique<BleServerSocket>();
server_socket->SetCloseNotifier([this, service_id]() {
absl::MutexLock lock(&mutex_);
server_sockets_.erase(service_id);
});
absl::MutexLock lock(&mutex_);
server_sockets_.insert({service_id, server_socket.get()});
return server_socket;
}
std::unique_ptr<api::ble_v2::BleSocket> BleMedium::Connect(const std::string& service_id,
TxPowerLevel tx_power_level,
api::ble_v2::BlePeripheral& peripheral,
CancellationFlag* cancellation_flag) {
NSString* serviceID = ObjCStringFromCppString(service_id);
__block std::unique_ptr<BleSocket> socket;
__block BlePeripheral ios_peripheral = static_cast<BlePeripheral&>(peripheral);
GNCMBleConnectionRequester connection_requester = ios_peripheral.GetConnectionRequester();
if (!connection_requester) return {};
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
if (cancellation_flag->Cancelled()) {
NSLog(@"[NEARBY] BLE Connect: Has been cancelled: service_id=%@", serviceID);
dispatch_group_leave(group); // unblock
return {};
}
connection_requester(serviceID, ^(id<GNCMConnection> connection) {
// If the connection wasn't successfully established, return a NULL socket.
if (connection) {
socket = std::make_unique<BleSocket>(connection, &ios_peripheral);
}
dispatch_group_leave(group); // unblock
return socket != nullptr
? static_cast<BleInputStream&>(socket->GetInputStream()).GetConnectionHandlers()
: nullptr;
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// Send the (empty) intro packet, which the BLE advertiser is expecting.
if (socket != nullptr) {
socket->GetOutputStream().Write(ByteArray());
}
return std::move(socket);
}
bool BleMedium::IsExtendedAdvertisementsAvailable() { return false; }
// NOLINTNEXTLINE
absl::optional<api::ble_v2::GattCharacteristic> BleMedium::GattServer::CreateCharacteristic(
const Uuid& service_uuid, const Uuid& characteristic_uuid,
api::ble_v2::GattCharacteristic::Permission permission,
api::ble_v2::GattCharacteristic::Property property) {
api::ble_v2::GattCharacteristic characteristic = {.uuid = characteristic_uuid,
.service_uuid = service_uuid,
.permission = permission,
.property = property};
[peripheral_
addCBServiceWithUUID:[CBUUID
UUIDWithString:ObjCStringFromCppString(
characteristic.service_uuid.Get16BitAsString())]];
[peripheral_
addCharacteristic:[[CBMutableCharacteristic alloc]
initWithType:[CBUUID UUIDWithString:ObjCStringFromCppString(std::string(
characteristic.uuid))]
properties:PropertiesToCBProperties(characteristic.property)
value:nil
permissions:PermissionToCBPermissions(characteristic.permission)]];
return characteristic;
}
bool BleMedium::GattServer::UpdateCharacteristic(
const api::ble_v2::GattCharacteristic& characteristic, const nearby::ByteArray& value) {
[peripheral_ updateValue:NSDataFromByteArray(value)
forCharacteristic:[CBUUID UUIDWithString:ObjCStringFromCppString(
std::string(characteristic.uuid))]];
return true;
}
absl::Status BleMedium::GattServer::NotifyCharacteristicChanged(
const api::ble_v2::GattCharacteristic& characteristic, bool confirm,
const ByteArray& new_value) {
// no-op because client cannot request notifications.
return absl::UnimplementedError("Unimplemented!");
}
void BleMedium::GattServer::Stop() { [peripheral_ stopGATTService]; }
bool BleMedium::GattClient::DiscoverServiceAndCharacteristics(
const Uuid& service_uuid, const std::vector<Uuid>& characteristic_uuids) {
// Discover all characteristics that may contain the advertisement.
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
gatt_characteristic_values_.clear();
CBUUID* serviceUUID = [CBUUID UUIDWithString:ObjCStringFromCppString(std::string(service_uuid))];
absl::flat_hash_map<std::string, Uuid> gatt_characteristics;
NSMutableArray<CBUUID*>* characteristicUUIDs =
[NSMutableArray arrayWithCapacity:characteristic_uuids.size()];
for (const auto& characteristic_uuid : characteristic_uuids) {
[characteristicUUIDs addObject:[CBUUID UUIDWithString:ObjCStringFromCppString(
std::string(characteristic_uuid))]];
gatt_characteristics.insert({std::string(characteristic_uuid), characteristic_uuid});
}
[central_ discoverGattService:serviceUUID
gattCharacteristics:characteristicUUIDs
peripheralID:ObjCStringFromCppString(peripheral_id_)
gattDiscoverResultHandler:^(NSOrderedSet<CBCharacteristic*>* _Nullable cb_characteristics) {
if (cb_characteristics != nil) {
for (CBCharacteristic* cb_characteristic in cb_characteristics) {
auto const& it = gatt_characteristics.find(
CppStringFromObjCString(cb_characteristic.UUID.UUIDString));
if (it == gatt_characteristics.end()) continue;
api::ble_v2::GattCharacteristic characteristic = {.uuid = it->second,
.service_uuid = service_uuid};
gatt_characteristic_values_.insert(
{characteristic, ByteArrayFromNSData(cb_characteristic.value).string_data()});
}
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC));
if (gatt_characteristic_values_.empty()) {
return false;
}
return true;
}
// NOLINTNEXTLINE
absl::optional<api::ble_v2::GattCharacteristic> BleMedium::GattClient::GetCharacteristic(
const Uuid& service_uuid, const Uuid& characteristic_uuid) {
api::ble_v2::GattCharacteristic characteristic = {.uuid = characteristic_uuid,
.service_uuid = service_uuid};
auto const it = gatt_characteristic_values_.find(characteristic);
if (it == gatt_characteristic_values_.end()) {
return absl::nullopt; // NOLINT
}
return it->first;
}
// NOLINTNEXTLINE
absl::optional<std::string> BleMedium::GattClient::ReadCharacteristic(
const api::ble_v2::GattCharacteristic& characteristic) {
auto const it = gatt_characteristic_values_.find(characteristic);
if (it == gatt_characteristic_values_.end()) {
return absl::nullopt; // NOLINT
}
return it->second;
}
bool BleMedium::GattClient::WriteCharacteristic(
const api::ble_v2::GattCharacteristic& characteristic, absl::string_view value,
api::ble_v2::GattClient::WriteType write_type) {
// No op.
return false;
}
bool BleMedium::GattClient::SetCharacteristicSubscription(
const api::ble_v2::GattCharacteristic& characteristic, bool enable,
absl::AnyInvocable<void(absl::string_view value)> on_characteristic_changed_cb) {
// No op since we can't write characteristics.
return false;
}
void BleMedium::GattClient::Disconnect() {
[central_ disconnectGattServiceWithPeripheralID:ObjCStringFromCppString(peripheral_id_)];
}
} // namespace apple
} // namespace nearby
@@ -0,0 +1,88 @@
// 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.
// Note: File language is detected using heuristics. Many Objective-C++ headers are incorrectly
// classified as C++ resulting in invalid linter errors. The use of "NSArray" and other Foundation
// classes like "NSData", "NSDictionary" and "NSUUID" are highly weighted for Objective-C and
// Objective-C++ scores. Oddly, "#import <Foundation/Foundation.h>" does not contribute any points.
// This comment alone should be enough to trick the IDE in to believing this is actually some sort
// of Objective-C file. See: cs/google3/devtools/search/lang/recognize_language_classifiers_data
#import <Foundation/Foundation.h>
#include <string>
#include <vector>
#include "internal/platform/implementation/ble_v2.h"
#include "internal/platform/uuid.h"
@class GNCBLEGATTClient;
namespace nearby {
namespace apple {
class GattClient : public api::ble_v2::GattClient {
public:
explicit GattClient(GNCBLEGATTClient *gatt_client_);
~GattClient() override = default;
// Discovers the specified characteristics of a service.
//
// This method blocks until discovery has finished.
//
// Returns whether or not discovery finished successfully.
bool DiscoverServiceAndCharacteristics(const Uuid &service_uuid,
const std::vector<Uuid> &characteristic_uuids) override;
// Retrieves a GATT characteristic.
//
// DiscoverServiceAndCharacteristics() must be called before this method to fetch all available
// services and characteristics first.
//
// On success, returns the characteristic. On error, returns nullptr.
std::optional<api::ble_v2::GattCharacteristic> GetCharacteristic(
const Uuid &service_uuid, const Uuid &characteristic_uuid) override;
// Reads the requested characteristic from the associated remote device.
//
// On success, returns the characteristic's value. On error, returns nullptr.
std::optional<std::string> ReadCharacteristic(
const api::ble_v2::GattCharacteristic &characteristic) override;
// Writes a given characteristic and its values to the associated remote device.
//
// Returns whether or not the write was successful.
bool WriteCharacteristic(const api::ble_v2::GattCharacteristic &characteristic,
absl::string_view value,
api::ble_v2::GattClient::WriteType type) override;
// Enable or disable notifications/indications for a given characteristic.
//
// Once notifications are enabled for a characteristic, on_characteristic_changed_cb will be
// triggered if the remote device indicates that the given characteristic has changed.
//
// Returns whether or not the subscription was successful.
bool SetCharacteristicSubscription(
const api::ble_v2::GattCharacteristic &characteristic, bool enable,
absl::AnyInvocable<void(absl::string_view value)> on_characteristic_changed_cb) override;
// Disconnects an established connection, or cancels a connection attempt currently in progress.
void Disconnect() override;
private:
GNCBLEGATTClient *gatt_client_;
};
} // namespace apple
} // namespace nearby
@@ -0,0 +1,134 @@
// 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/ble_gatt_client.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#include <string>
#include <utility>
#include <vector>
#include "internal/platform/implementation/ble_v2.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTClient.h"
#import "internal/platform/implementation/apple/ble_utils.h"
#import "GoogleToolboxForMac/GTMLogger.h"
namespace nearby {
namespace apple {
GattClient::GattClient(GNCBLEGATTClient *gatt_client) : gatt_client_(gatt_client) {}
bool GattClient::DiscoverServiceAndCharacteristics(const Uuid &service_uuid,
const std::vector<Uuid> &characteristic_uuids) {
CBUUID *serviceUUID = CBUUID128FromCPP(service_uuid);
NSMutableArray<CBUUID *> *characteristics = [[NSMutableArray alloc] init];
for (const auto &uuid : characteristic_uuids) {
[characteristics addObject:CBUUID128FromCPP(uuid)];
}
NSCondition *condition = [[NSCondition alloc] init];
[condition lock];
__block NSError *blockError = nil;
[gatt_client_ discoverCharacteristicsWithUUIDs:characteristics
serviceUUID:serviceUUID
completionHandler:^(NSError *error) {
[condition lock];
if (error != nil) {
GTMLoggerError(@"Error discovering characteristics: %@", error);
}
blockError = error;
[condition signal];
[condition unlock];
}];
[condition wait];
[condition unlock];
return blockError == nil;
}
std::optional<api::ble_v2::GattCharacteristic> GattClient::GetCharacteristic(
const Uuid &service_uuid, const Uuid &characteristic_uuid) {
CBUUID *serviceUUID = CBUUID128FromCPP(service_uuid);
CBUUID *characteristicUUID = CBUUID128FromCPP(characteristic_uuid);
NSCondition *condition = [[NSCondition alloc] init];
[condition lock];
__block GNCBLEGATTCharacteristic *blockCharacteristic = nil;
__block NSError *blockError = nil;
[gatt_client_ characteristicWithUUID:characteristicUUID
serviceUUID:serviceUUID
completionHandler:^(GNCBLEGATTCharacteristic *characteristic, NSError *error) {
[condition lock];
if (error != nil) {
GTMLoggerError(@"Error retrieving characteristic: %@", error);
}
blockCharacteristic = characteristic;
blockError = error;
[condition signal];
[condition unlock];
}];
[condition wait];
[condition unlock];
if (blockCharacteristic == nil) {
return std::nullopt;
}
return CPPGATTCharacteristicFromObjC(blockCharacteristic);
}
std::optional<std::string> GattClient::ReadCharacteristic(
const api::ble_v2::GattCharacteristic &characteristic) {
NSCondition *condition = [[NSCondition alloc] init];
[condition lock];
__block NSData *blockValue = nil;
__block NSError *blockError = nil;
[gatt_client_ readValueForCharacteristic:ObjCGATTCharacteristicFromCPP(characteristic)
completionHandler:^(NSData *value, NSError *error) {
[condition lock];
if (error != nil) {
GTMLoggerError(@"Error reading characteristic: %@", error);
}
blockValue = value;
blockError = error;
[condition signal];
[condition unlock];
}];
[condition wait];
[condition unlock];
if (blockValue == nil) {
return std::nullopt;
}
return std::string((const char *)blockValue.bytes, blockValue.length);
}
// TODO(b/290385712): Implement.
bool GattClient::WriteCharacteristic(const api::ble_v2::GattCharacteristic &characteristic,
absl::string_view value,
api::ble_v2::GattClient::WriteType type) {
return false;
}
// TODO(b/290385712): Implement.
bool GattClient::SetCharacteristicSubscription(
const api::ble_v2::GattCharacteristic &characteristic, bool enable,
absl::AnyInvocable<void(absl::string_view value)> on_characteristic_changed_cb) {
return false;
}
// TODO(b/290385712): Implement.
void GattClient::Disconnect() {}
} // namespace apple
} // namespace nearby
@@ -0,0 +1,78 @@
// 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.
// Note: File language is detected using heuristics. Many Objective-C++ headers are incorrectly
// classified as C++ resulting in invalid linter errors. The use of "NSArray" and other Foundation
// classes like "NSData", "NSDictionary" and "NSUUID" are highly weighted for Objective-C and
// Objective-C++ scores. Oddly, "#import <Foundation/Foundation.h>" does not contribute any points.
// This comment alone should be enough to trick the IDE in to believing this is actually some sort
// of Objective-C file. See: cs/google3/devtools/search/lang/recognize_language_classifiers_data
#import <Foundation/Foundation.h>
#include "internal/platform/implementation/ble_v2.h"
#include "internal/platform/uuid.h"
#import "internal/platform/implementation/apple/ble_peripheral.h"
@class GNCBLEGATTServer;
namespace nearby {
namespace apple {
class GattServer : public api::ble_v2::GattServer {
public:
explicit GattServer(GNCBLEGATTServer *gatt_server_);
~GattServer() override = default;
// Returns an empty BlePeripheral object.
//
// Use of this method should be avoided and its only purpose seems to be a check that the GATT
// server is valid.
api::ble_v2::BlePeripheral &GetBlePeripheral() override;
// Creates a characteristic and adds it to the GATT server under the given characteristic and
// service UUIDs.
//
// Characteristics of the same service UUID will be put under one service rather than many
// services with the same UUID.
//
// Returns no value upon error.
std::optional<api::ble_v2::GattCharacteristic> CreateCharacteristic(
const Uuid &service_uuid, const Uuid &characteristic_uuid,
api::ble_v2::GattCharacteristic::Permission permission,
api::ble_v2::GattCharacteristic::Property property) override;
// Updates a local characteristic with the provided value.
//
// Returns whether or not the update was successful.
bool UpdateCharacteristic(const api::ble_v2::GattCharacteristic &characteristic,
const nearby::ByteArray &value) override;
// Send a notification or indication that a local characteristic has been updated.
//
// Returns an absl::Status indicating success or what went wrong.
absl::Status NotifyCharacteristicChanged(const api::ble_v2::GattCharacteristic &characteristic,
bool confirm, const ByteArray &new_value) override;
// Stops a GATT server.
void Stop() override;
private:
GNCBLEGATTServer *gatt_server_;
EmptyBlePeripheral peripheral_;
};
} // namespace apple
} // namespace nearby
@@ -0,0 +1,104 @@
// 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/ble_gatt_server.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#include "internal/platform/implementation/ble_v2.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h"
#import "internal/platform/implementation/apple/ble_utils.h"
#import "internal/platform/implementation/apple/utils.h"
#import "GoogleToolboxForMac/GTMLogger.h"
namespace nearby {
namespace apple {
GattServer::GattServer(GNCBLEGATTServer *gatt_server) : gatt_server_(gatt_server) {}
std::optional<api::ble_v2::GattCharacteristic> GattServer::CreateCharacteristic(
const Uuid &service_uuid, const Uuid &characteristic_uuid,
api::ble_v2::GattCharacteristic::Permission permission,
api::ble_v2::GattCharacteristic::Property property) {
CBUUID *serviceUUID = CBUUID128FromCPP(service_uuid);
CBUUID *characteristicUUID = CBUUID128FromCPP(characteristic_uuid);
CBAttributePermissions permissions = CBAttributePermissionsFromCPP(permission);
CBCharacteristicProperties properties = CBCharacteristicPropertiesFromCPP(property);
NSCondition *condition = [[NSCondition alloc] init];
[condition lock];
__block GNCBLEGATTCharacteristic *blockCharacteristic = nil;
[gatt_server_ createCharacteristicWithServiceID:serviceUUID
characteristicUUID:characteristicUUID
permissions:permissions
properties:properties
completionHandler:^(GNCBLEGATTCharacteristic *characteristic,
NSError *error) {
[condition lock];
if (error != nil) {
GTMLoggerError(@"Error creating characteristic: %@", error);
}
blockCharacteristic = characteristic;
[condition signal];
[condition unlock];
}];
[condition wait];
[condition unlock];
if (blockCharacteristic == nil) {
return std::nullopt;
}
return CPPGATTCharacteristicFromObjC(blockCharacteristic);
}
bool GattServer::UpdateCharacteristic(const api::ble_v2::GattCharacteristic &characteristic,
const nearby::ByteArray &value) {
NSCondition *condition = [[NSCondition alloc] init];
[condition lock];
__block NSError *blockError = nil;
[gatt_server_ updateCharacteristic:ObjCGATTCharacteristicFromCPP(characteristic)
value:NSDataFromByteArray(value)
completionHandler:^(NSError *error) {
[condition lock];
if (error != nil) {
GTMLoggerError(@"Error updating characteristic: %@", error);
}
blockError = error;
[condition signal];
[condition unlock];
}];
[condition wait];
[condition unlock];
return blockError == nil;
}
// TODO(b/290385712): Implement.
absl::Status GattServer::NotifyCharacteristicChanged(
const api::ble_v2::GattCharacteristic &characteristic, bool confirm,
const ByteArray &new_value) {
return absl::UnimplementedError("");
}
void GattServer::Stop() {
[gatt_server_ stop];
}
// TODO(b/290385712): Implement.
api::ble_v2::BlePeripheral &GattServer::GetBlePeripheral() {
return peripheral_;
}
} // namespace apple
} // namespace nearby
@@ -0,0 +1,164 @@
// Copyright 2022 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.
// Note: File language is detected using heuristics. Many Objective-C++ headers are incorrectly
// classified as C++ resulting in invalid linter errors. The use of "NSArray" and other Foundation
// classes like "NSData", "NSDictionary" and "NSUUID" are highly weighted for Objective-C and
// Objective-C++ scores. Oddly, "#import <Foundation/Foundation.h>" does not contribute any points.
// This comment alone should be enough to trick the IDE in to believing this is actually some sort
// of Objective-C file. See: cs/google3/devtools/search/lang/recognize_language_classifiers_data
#import <Foundation/Foundation.h>
#include <memory>
#include <string>
#include "internal/platform/cancellation_flag.h"
#include "internal/platform/implementation/ble_v2.h"
#include "internal/platform/implementation/bluetooth_adapter.h"
#include "internal/platform/uuid.h"
#import "internal/platform/implementation/apple/ble_peripheral.h"
#import "internal/platform/implementation/apple/ble_server_socket.h"
#import "internal/platform/implementation/apple/bluetooth_adapter_v2.h"
@class GNCBLEMedium;
@class GNSCentralManager;
@class GNSPeripheralManager;
@class GNSPeripheralServiceManager;
namespace nearby {
namespace apple {
// The main BLE medium used inside of Nearby. This serves as the entry point for all BLE and GATT
// related operations.
class BleMedium : public api::ble_v2::BleMedium {
public:
BleMedium();
~BleMedium() override = default;
// Async interface for StartAdvertising.
//
// Result status will be passed to start_advertising_result callback. To stop advertising, invoke
// the stop_advertising callback in AdvertisingSession.
//
// Advertising must be stopped before attempting to start advertising again.
std::unique_ptr<api::ble_v2::BleMedium::AdvertisingSession> StartAdvertising(
const api::ble_v2::BleAdvertisementData &advertising_data,
api::ble_v2::AdvertiseParameters advertise_set_parameters,
api::ble_v2::BleMedium::AdvertisingCallback callback) override;
// Starts BLE advertising and returns whether or not it was successful.
//
// Advertising must be stopped before attempting to start advertising again.
bool StartAdvertising(const api::ble_v2::BleAdvertisementData &advertising_data,
api::ble_v2::AdvertiseParameters advertise_set_parameters) override;
// Stops advertising.
//
// Returns whether or not advertising was successfully stopped.
bool StopAdvertising() override;
// Async interface for StartScanning.
//
// Result status will be passed to start_scanning_result callback on a private queue. To stop
// scanning, invoke the stop_scanning callback in ScanningSession.
//
// Scanning must be stopped before attempting to start scanning again.
std::unique_ptr<api::ble_v2::BleMedium::ScanningSession> StartScanning(
const Uuid &service_uuid, api::ble_v2::TxPowerLevel tx_power_level,
api::ble_v2::BleMedium::ScanningCallback callback) override;
// Starts scanning and returns whether or not it was successful.
//
// Scanning must be stopped before attempting to start scanning again.
bool StartScanning(const Uuid &service_uuid, api::ble_v2::TxPowerLevel tx_power_level,
api::ble_v2::BleMedium::ScanCallback callback) override;
// Stops scanning.
//
// Returns whether or not scanning was successfully stopped.
bool StopScanning() override;
// TODO(b/290385712): ServerGattConnectionCallback methods are not yet implemented.
//
// Starts a GATT server. Returns a nullptr upon error.
std::unique_ptr<api::ble_v2::GattServer> StartGattServer(
api::ble_v2::ServerGattConnectionCallback callback) override;
// Connects to a GATT server and negotiates the specified connection parameters. Returns nullptr
// upon error.
//
// The peripheral must outlive the GATT client or undefined behavior will occur. The peripheral
// should not be modified by this method.
std::unique_ptr<api::ble_v2::GattClient> ConnectToGattServer(
api::ble_v2::BlePeripheral &peripheral, api::ble_v2::TxPowerLevel tx_power_level,
api::ble_v2::ClientGattConnectionCallback callback) override;
// Opens a BLE server socket based on service ID.
//
// On success, returns a new BleServerSocket. On error, returns nullptr.
std::unique_ptr<api::ble_v2::BleServerSocket> OpenServerSocket(
const std::string &service_id) override;
// TODO(b/290385712): cancellation_flag support is not yet implemented.
//
// Connects to a BLE peripheral.
//
// The peripheral must outlive the socket or undefined behavior will occur. The peripheral
// should not be modified by this method.
//
// On success, returns a new BleSocket. On error, returns nullptr.
std::unique_ptr<api::ble_v2::BleSocket> Connect(const std::string &service_id,
api::ble_v2::TxPowerLevel tx_power_level,
api::ble_v2::BlePeripheral &peripheral,
CancellationFlag *cancellation_flag) override;
// Returns whether the hardware supports BOTH advertising extensions and extended scans.
//
// This is currently always false for all Apple hardware.
bool IsExtendedAdvertisementsAvailable() override;
// A peripheral cannot be retreived via MAC address on Apple platforms.
//
// This always returns false and does not call the callback.
bool GetRemotePeripheral(const std::string &mac_address,
api::ble_v2::BleMedium::GetRemotePeripheralCallback callback) override;
// Returns true if `id` refers to a known BLE peripheral and calls `callback` with a reference to
// said peripheral that is only guaranteed to be available for the duration of the callback.
// Otherwise, does not call the callback and returns false.
bool GetRemotePeripheral(api::ble_v2::BlePeripheral::UniqueId id,
api::ble_v2::BleMedium::GetRemotePeripheralCallback callback) override;
private:
void HandleAdvertisementFound(
id<GNCPeripheral> peripheral, NSDictionary<CBUUID *, NSData *> *serviceData,
absl::AnyInvocable<void(api::ble_v2::BlePeripheral &peripheral,
api::ble_v2::BleAdvertisementData advertisement_data)>
callback);
GNCBLEMedium *medium_;
absl::Mutex peripherals_mutex_;
absl::flat_hash_map<api::ble_v2::BlePeripheral::UniqueId, std::unique_ptr<BlePeripheral>>
peripherals_ ABSL_GUARDED_BY(peripherals_mutex_);
GNSPeripheralServiceManager *socketPeripheralServiceManager_;
GNSPeripheralManager *socketPeripheralManager_;
GNSCentralManager *socketCentralManager_;
};
} // namespace apple
} // namespace nearby
@@ -0,0 +1,384 @@
// Copyright 2022 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/ble_medium.h"
#import "internal/platform/implementation/apple/utils.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "internal/platform/implementation/apple/ble_utils.h"
#include "internal/platform/implementation/apple/utils.h"
#include "internal/platform/implementation/ble_v2.h"
#include "internal/platform/implementation/bluetooth_adapter.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTClient.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTServer.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEMedium.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h"
#import "internal/platform/implementation/apple/ble_gatt_client.h"
#import "internal/platform/implementation/apple/ble_gatt_server.h"
#import "internal/platform/implementation/apple/ble_peripheral.h"
#import "internal/platform/implementation/apple/ble_server_socket.h"
#import "internal/platform/implementation/apple/ble_socket.h"
#import "internal/platform/implementation/apple/bluetooth_adapter_v2.h"
#import "GoogleToolboxForMac/GTMLogger.h"
// TODO(b/293336684): Old Weave imports that need to be deleted once shared Weave is complete.
#import "internal/platform/implementation/apple/Mediums/Ble/GNCMBleConnection.h"
#import "internal/platform/implementation/apple/Mediums/Ble/GNCMBleUtils.h"
#import "internal/platform/implementation/apple/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.h"
#import "internal/platform/implementation/apple/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.h"
#import "internal/platform/implementation/apple/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager.h"
#import "internal/platform/implementation/apple/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager.h"
static NSString *const kWeaveServiceUUID = @"FEF3";
namespace nearby {
namespace apple {
BleMedium::BleMedium() : medium_([[GNCBLEMedium alloc] init]) {}
std::unique_ptr<api::ble_v2::BleMedium::AdvertisingSession> BleMedium::StartAdvertising(
const api::ble_v2::BleAdvertisementData &advertising_data,
api::ble_v2::AdvertiseParameters advertise_set_parameters,
api::ble_v2::BleMedium::AdvertisingCallback callback) {
NSMutableDictionary<CBUUID *, NSData *> *serviceData =
ObjCServiceDataFromCPP(advertising_data.service_data);
__block api::ble_v2::BleMedium::AdvertisingCallback blockCallback = std::move(callback);
[medium_ startAdvertisingData:serviceData
completionHandler:^(NSError *error) {
blockCallback.start_advertising_result(
error == nil ? absl::OkStatus()
: absl::InternalError(error.localizedDescription.UTF8String));
}];
return std::make_unique<AdvertisingSession>(AdvertisingSession{.stop_advertising = [this] {
return StopAdvertising() ? absl::OkStatus() : absl::InternalError("Failed to stop advertising");
}});
}
bool BleMedium::StartAdvertising(const api::ble_v2::BleAdvertisementData &advertising_data,
api::ble_v2::AdvertiseParameters advertise_set_parameters) {
NSMutableDictionary<CBUUID *, NSData *> *serviceData =
ObjCServiceDataFromCPP(advertising_data.service_data);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSError *blockError = nil;
[medium_ startAdvertisingData:serviceData
completionHandler:^(NSError *error) {
if (error != nil) {
GTMLoggerError(@"Failed to start advertising: %@", error);
}
blockError = error;
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return blockError == nil;
}
bool BleMedium::StopAdvertising() {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSError *blockError = nil;
[medium_ stopAdvertisingWithCompletionHandler:^(NSError *error) {
if (error != nil) {
GTMLoggerError(@"Failed to stop advertising: %@", error);
}
blockError = error;
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return blockError == nil;
}
void BleMedium::HandleAdvertisementFound(
id<GNCPeripheral> peripheral, NSDictionary<CBUUID *, NSData *> *serviceData,
absl::AnyInvocable<void(api::ble_v2::BlePeripheral &peripheral,
api::ble_v2::BleAdvertisementData advertisement_data)>
callback) {
absl::MutexLock lock(&peripherals_mutex_);
[socketCentralManager_ retrievePeripheralWithIdentifier:peripheral.identifier
advertisementData:@{}];
api::ble_v2::BleAdvertisementData data;
for (CBUUID *key in serviceData.allKeys) {
data.service_data[CPPUUIDFromObjC(key)] = ByteArrayFromNSData(serviceData[key]);
}
// Add the peripheral to the map if we haven't discovered it yet.
auto ble_peripheral = std::make_unique<BlePeripheral>(peripheral);
auto unique_id = ble_peripheral->GetUniqueId();
auto it = peripherals_.find(unique_id);
if (it == peripherals_.end()) {
peripherals_[unique_id] = std::move(ble_peripheral);
}
callback(*peripherals_[unique_id], data);
}
std::unique_ptr<api::ble_v2::BleMedium::ScanningSession> BleMedium::StartScanning(
const Uuid &service_uuid, api::ble_v2::TxPowerLevel tx_power_level,
api::ble_v2::BleMedium::ScanningCallback callback) {
CBUUID *serviceUUID = CBUUID128FromCPP(service_uuid);
__block api::ble_v2::BleMedium::ScanningCallback blockCallback = std::move(callback);
socketCentralManager_ = [[GNSCentralManager alloc] initWithSocketServiceUUID:serviceUUID];
[socketCentralManager_ startNoScanModeWithAdvertisedServiceUUIDs:@[ serviceUUID ]];
[medium_ startScanningForService:serviceUUID
advertisementFoundHandler:^(id<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *serviceData) {
HandleAdvertisementFound(peripheral, serviceData,
std::move(blockCallback.advertisement_found_cb));
}
completionHandler:^(NSError *error) {
blockCallback.start_scanning_result(
error == nil ? absl::OkStatus()
: absl::InternalError(error.localizedDescription.UTF8String));
}];
return std::make_unique<ScanningSession>(ScanningSession{.stop_scanning = [this] {
return StopScanning() ? absl::OkStatus() : absl::InternalError("Failed to stop scanning");
}});
}
bool BleMedium::StartScanning(const Uuid &service_uuid, api::ble_v2::TxPowerLevel tx_power_level,
api::ble_v2::BleMedium::ScanCallback callback) {
CBUUID *serviceUUID = CBUUID128FromCPP(service_uuid);
__block api::ble_v2::BleMedium::ScanCallback blockCallback = std::move(callback);
socketCentralManager_ = [[GNSCentralManager alloc] initWithSocketServiceUUID:serviceUUID];
[socketCentralManager_ startNoScanModeWithAdvertisedServiceUUIDs:@[ serviceUUID ]];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSError *blockError = nil;
[medium_ startScanningForService:serviceUUID
advertisementFoundHandler:^(id<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *serviceData) {
HandleAdvertisementFound(peripheral, serviceData,
std::move(blockCallback.advertisement_found_cb));
}
completionHandler:^(NSError *error) {
if (error != nil) {
GTMLoggerError(@"Failed to start scanning: %@", error);
}
blockError = error;
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return blockError == nil;
}
bool BleMedium::StopScanning() {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSError *blockError = nil;
[medium_ stopScanningWithCompletionHandler:^(NSError *error) {
if (error != nil) {
GTMLoggerError(@"Failed to stop scanning: %@", error);
}
blockError = error;
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return blockError == nil;
}
// TODO(b/290385712): Add implementation that calls ServerGattConnectionCallback methods.
std::unique_ptr<api::ble_v2::GattServer> BleMedium::StartGattServer(
api::ble_v2::ServerGattConnectionCallback callback) {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block GNCBLEGATTServer *blockServer = nil;
[medium_ startGATTServerWithCompletionHandler:^(GNCBLEGATTServer *server, NSError *error) {
if (error != nil) {
GTMLoggerError(@"Error starting GATT server: %@", error);
}
blockServer = server;
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (!blockServer) {
return nullptr;
}
return std::make_unique<GattServer>(blockServer);
}
std::unique_ptr<api::ble_v2::GattClient> BleMedium::ConnectToGattServer(
api::ble_v2::BlePeripheral &peripheral, api::ble_v2::TxPowerLevel tx_power_level,
api::ble_v2::ClientGattConnectionCallback callback) {
// Check that the @c api::ble_v2::BlePeripheral is a @c nearby::apple::BlePeripheral and not a
// @c nearby::apple::EmptyBlePeripheral instance, so we can retreive the CBPeripheral object.
BlePeripheral *non_empty_peripheral = dynamic_cast<BlePeripheral *>(&peripheral);
if (non_empty_peripheral == nullptr) {
return nullptr;
}
__block api::ble_v2::ClientGattConnectionCallback blockCallback = std::move(callback);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block GNCBLEGATTClient *blockClient = nil;
[medium_ connectToGATTServerForPeripheral:non_empty_peripheral->GetPeripheral()
disconnectionHandler:^(void) {
blockCallback.disconnected_cb();
}
completionHandler:^(GNCBLEGATTClient *client, NSError *error) {
if (error != nil) {
GTMLoggerError(@"Error connecting to GATT server: %@", error);
}
blockClient = client;
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (!blockClient) {
return nullptr;
}
return std::make_unique<GattClient>(blockClient);
}
// TODO(b/293336684): Old Weave code that need to be deleted once shared Weave is complete.
std::unique_ptr<api::ble_v2::BleServerSocket> BleMedium::OpenServerSocket(
const std::string &service_id) {
__block auto server_socket = std::make_unique<BleServerSocket>();
socketPeripheralServiceManager_ = [[GNSPeripheralServiceManager alloc]
initWithBleServiceUUID:[CBUUID UUIDWithString:kWeaveServiceUUID]
addPairingCharacteristic:NO
shouldAcceptSocketHandler:^BOOL(GNSSocket *socket) {
GNCMWaitForConnection(socket, ^(BOOL didConnect) {
GNCMBleConnection *connection =
[GNCMBleConnection connectionWithSocket:socket
serviceID:@(service_id.c_str())
expectedIntroPacket:YES
callbackQueue:dispatch_get_main_queue()];
auto socket = std::make_unique<BleSocket>(connection);
connection.connectionHandlers = socket->GetInputStream().GetConnectionHandlers();
server_socket->Connect(std::move(socket));
});
return YES;
}];
socketPeripheralManager_ = [[GNSPeripheralManager alloc] initWithAdvertisedName:nil
restoreIdentifier:nil];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSError *blockError = nil;
[socketPeripheralManager_ addPeripheralServiceManager:socketPeripheralServiceManager_
bleServiceAddedCompletion:^(NSError *error) {
if (error != nil) {
GTMLoggerError(@"Failed to add Weave service: %@", error);
}
blockError = error;
dispatch_semaphore_signal(semaphore);
}];
[socketPeripheralManager_ start];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (blockError != nil) {
return nullptr;
}
return std::move(server_socket);
}
// TODO(b/290385712): Add support for @c cancellation_flag.
// TODO(b/293336684): Old Weave code that need to be deleted once shared Weave is complete.
std::unique_ptr<api::ble_v2::BleSocket> BleMedium::Connect(const std::string &service_id,
api::ble_v2::TxPowerLevel tx_power_level,
api::ble_v2::BlePeripheral &peripheral,
CancellationFlag *cancellation_flag) {
// Check that the @c api::ble_v2::BlePeripheral is a @c nearby::apple::BlePeripheral and not a
// @c nearby::apple::EmptyBlePeripheral instance, so we can retreive the CBPeripheral object.
BlePeripheral *non_empty_peripheral = dynamic_cast<BlePeripheral *>(&peripheral);
if (non_empty_peripheral == nullptr) {
return nullptr;
}
GNSCentralPeerManager *updatedCentralPeerManager = [socketCentralManager_
retrieveCentralPeerWithIdentifier:non_empty_peripheral->GetPeripheral().identifier];
if (!updatedCentralPeerManager) {
return nullptr;
}
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block std::unique_ptr<BleSocket> socket;
[updatedCentralPeerManager
socketWithPairingCharacteristic:NO
completion:^(GNSSocket *nssocket, NSError *error) {
if (error) {
dispatch_semaphore_signal(semaphore);
return;
}
GNCMWaitForConnection(nssocket, ^(BOOL didConnect) {
if (!didConnect) {
dispatch_semaphore_signal(semaphore);
return;
}
GNCMBleConnection *connection = [GNCMBleConnection
connectionWithSocket:nssocket
serviceID:@(service_id.c_str())
expectedIntroPacket:NO
callbackQueue:dispatch_get_main_queue()];
socket =
std::make_unique<BleSocket>(connection, non_empty_peripheral);
connection.connectionHandlers =
socket->GetInputStream().GetConnectionHandlers();
dispatch_semaphore_signal(semaphore);
});
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (socket == nullptr) {
return nullptr;
}
// Send the (empty) intro packet, which the BLE advertiser is expecting.
socket->GetOutputStream().Write(ByteArray());
return std::move(socket);
}
bool BleMedium::IsExtendedAdvertisementsAvailable() {
return [medium_ supportsExtendedAdvertisements];
}
bool BleMedium::GetRemotePeripheral(const std::string &mac_address,
api::ble_v2::BleMedium::GetRemotePeripheralCallback callback) {
// Apple does not expose MAC address information, so we cannot retreive a peripheral via MAC
// address.
return false;
}
bool BleMedium::GetRemotePeripheral(api::ble_v2::BlePeripheral::UniqueId unique_id,
api::ble_v2::BleMedium::GetRemotePeripheralCallback callback) {
BlePeripheral *peripheral;
{
absl::MutexLock lock(&peripherals_mutex_);
auto it = peripherals_.find(unique_id);
if (it == peripherals_.end()) {
return false;
}
peripheral = it->second.get();
if (peripheral == nullptr) {
return false;
}
}
// We need to unlock before calling the callback, otherwise we will deadlock.
callback(*peripheral);
return true;
}
} // namespace apple
} // namespace nearby
@@ -0,0 +1,84 @@
// 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.
// Note: File language is detected using heuristics. Many Objective-C++ headers are incorrectly
// classified as C++ resulting in invalid linter errors. The use of "NSArray" and other Foundation
// classes like "NSData", "NSDictionary" and "NSUUID" are highly weighted for Objective-C and
// Objective-C++ scores. Oddly, "#import <Foundation/Foundation.h>" does not contribute any points.
// This comment alone should be enough to trick the IDE in to believing this is actually some sort
// of Objective-C file. See: cs/google3/devtools/search/lang/recognize_language_classifiers_data
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#include <string>
#include "internal/platform/implementation/ble_v2.h"
@protocol GNCPeripheral;
namespace nearby {
namespace apple {
// An empty peripheral.
//
// Apple APIs do not expose a peripheral's MAC address and does not provide a
// way to directly connect to a given MAC address. Instead a connection can only
// be made using a CoreBluetooth peripheral object. Many times a CoreBluetooth
// peripheral is not available, namely, when the remote device is a central. For
// these cases, an EmptyBlePeripheral should be used.
class EmptyBlePeripheral : public api::ble_v2::BlePeripheral {
public:
EmptyBlePeripheral();
~EmptyBlePeripheral() override = default;
// Returns an empty string.
std::string GetAddress() const override;
// Returns an immutable unique identifier. The identifier does not change when
// the peripheral's address is rotated.
api::ble_v2::BlePeripheral::UniqueId GetUniqueId() const override;
private:
api::ble_v2::BlePeripheral::UniqueId unique_id_;
};
// A wrapper of a CoreBluetooth peripheral object. This can be used to uniquely
// identify a peripheral and connect to its GATT server.
//
// Many times a CoreBluetooth peripheral is not available, namely, when the
// remote device is a central. For these cases, an EmptyBlePeripheral should be
// used instead.
class BlePeripheral : public api::ble_v2::BlePeripheral {
public:
explicit BlePeripheral(id<GNCPeripheral> peripheral);
~BlePeripheral() override = default;
// Returns an empty string.
std::string GetAddress() const override;
// Returns an immutable unique identifier. The identifier does not change when
// the peripheral's address is rotated.
api::ble_v2::BlePeripheral::UniqueId GetUniqueId() const override;
// Returns the CoreBluetooth peripheral object.
id<GNCPeripheral> GetPeripheral() const;
private:
id<GNCPeripheral> peripheral_;
api::ble_v2::BlePeripheral::UniqueId unique_id_;
};
} // namespace apple
} // namespace nearby
@@ -0,0 +1,50 @@
// 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/ble_peripheral.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#include <string>
#include "internal/platform/implementation/ble_v2.h"
#include "internal/platform/prng.h"
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCPeripheral.h"
namespace nearby {
namespace apple {
#pragma mark - EmptyBlePeripheral
EmptyBlePeripheral::EmptyBlePeripheral() : unique_id_(Prng().NextInt64()) {}
std::string EmptyBlePeripheral::GetAddress() const { return ""; }
api::ble_v2::BlePeripheral::UniqueId EmptyBlePeripheral::GetUniqueId() const { return unique_id_; }
#pragma mark - BlePeripheral
BlePeripheral::BlePeripheral(id<GNCPeripheral> peripheral)
: peripheral_(peripheral), unique_id_(peripheral.identifier.hash) {}
std::string BlePeripheral::GetAddress() const { return ""; }
api::ble_v2::BlePeripheral::UniqueId BlePeripheral::GetUniqueId() const { return unique_id_; }
id<GNCPeripheral> BlePeripheral::GetPeripheral() const { return peripheral_; }
} // namespace apple
} // namespace nearby
@@ -0,0 +1,76 @@
// 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.
// Note: File language is detected using heuristics. Many Objective-C++ headers are incorrectly
// classified as C++ resulting in invalid linter errors. The use of "NSArray" and other Foundation
// classes like "NSData", "NSDictionary" and "NSUUID" are highly weighted for Objective-C and
// Objective-C++ scores. Oddly, "#import <Foundation/Foundation.h>" does not contribute any points.
// This comment alone should be enough to trick the IDE in to believing this is actually some sort
// of Objective-C file. See: cs/google3/devtools/search/lang/recognize_language_classifiers_data
// TODO(b/293336684): Remove this file when shared Weave is complete.
#import <Foundation/Foundation.h>
#include <memory>
#include "absl/functional/any_invocable.h"
#include "internal/platform/exception.h"
#include "internal/platform/implementation/ble_v2.h"
#import "internal/platform/implementation/apple/ble_socket.h"
namespace nearby {
namespace apple {
// A BLE server socket for listening for incoming Weave sockets.
class BleServerSocket : public api::ble_v2::BleServerSocket {
public:
BleServerSocket() = default;
~BleServerSocket() override;
// Wait for an available socket.
//
// Blocks until either:
// - at least one incoming connection request is available, or
// - ServerSocket is closed.
//
// On success, returns connected socket, ready to exchange data or nullptr on
// error. Once error is reported, it is permanent, and ServerSocket must be
// closed.
std::unique_ptr<api::ble_v2::BleSocket> Accept() override
ABSL_LOCKS_EXCLUDED(mutex_);
// Close the server socket.
//
// Returns Exception::kIo on error, otherwise Exception::kSuccess.
Exception Close() override ABSL_LOCKS_EXCLUDED(mutex_);
bool Connect(std::unique_ptr<BleSocket> socket) ABSL_LOCKS_EXCLUDED(mutex_);
void SetCloseNotifier(absl::AnyInvocable<void()> notifier)
ABSL_LOCKS_EXCLUDED(mutex_);
private:
Exception DoClose() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
mutable absl::Mutex mutex_;
absl::CondVar cond_;
absl::flat_hash_set<std::unique_ptr<BleSocket>> pending_sockets_
ABSL_GUARDED_BY(mutex_);
absl::AnyInvocable<void()> close_notifier_ ABSL_GUARDED_BY(mutex_);
bool closed_ ABSL_GUARDED_BY(mutex_) = false;
};
} // namespace apple
} // namespace nearby
@@ -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.
// TODO(b/293336684): Remove this file when shared Weave is complete.
#import "internal/platform/implementation/apple/ble_server_socket.h"
#import <Foundation/Foundation.h>
#include <memory>
#include <utility>
namespace nearby {
namespace apple {
BleServerSocket::~BleServerSocket() {
absl::MutexLock lock(&mutex_);
DoClose();
}
std::unique_ptr<api::ble_v2::BleSocket> BleServerSocket::Accept() {
absl::MutexLock lock(&mutex_);
while (!closed_ && pending_sockets_.empty()) {
cond_.Wait(&mutex_);
}
if (closed_) return {};
std::unique_ptr<BleSocket> remote_socket =
std::move(pending_sockets_.extract(pending_sockets_.begin()).value());
return std::move(remote_socket);
}
bool BleServerSocket::Connect(std::unique_ptr<BleSocket> socket) {
absl::MutexLock lock(&mutex_);
if (closed_) {
return false;
}
pending_sockets_.insert(std::move(socket));
cond_.SignalAll();
return !closed_;
}
void BleServerSocket::SetCloseNotifier(absl::AnyInvocable<void()> notifier) {
absl::MutexLock lock(&mutex_);
close_notifier_ = std::move(notifier);
}
Exception BleServerSocket::Close() {
absl::MutexLock lock(&mutex_);
return DoClose();
}
Exception BleServerSocket::DoClose() {
bool should_notify = !closed_;
closed_ = true;
if (should_notify) {
cond_.SignalAll();
if (close_notifier_) {
auto notifier = std::move(close_notifier_);
mutex_.Unlock();
// Notifier may contain calls to public API, and may cause deadlock, if
// mutex_ is held during the call.
notifier();
mutex_.Lock();
}
}
return {Exception::kSuccess};
}
} // namespace apple
} // namespace nearby
@@ -0,0 +1,134 @@
// 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.
// Note: File language is detected using heuristics. Many Objective-C++ headers are incorrectly
// classified as C++ resulting in invalid linter errors. The use of "NSArray" and other Foundation
// classes like "NSData", "NSDictionary" and "NSUUID" are highly weighted for Objective-C and
// Objective-C++ scores. Oddly, "#import <Foundation/Foundation.h>" does not contribute any points.
// This comment alone should be enough to trick the IDE in to believing this is actually some sort
// of Objective-C file. See: cs/google3/devtools/search/lang/recognize_language_classifiers_data
// TODO(b/293336684): Remove this file when shared Weave is complete.
#import <Foundation/Foundation.h>
#include <memory>
#include "internal/platform/implementation/ble_v2.h"
#import "internal/platform/implementation/apple/ble_peripheral.h"
@class GNCMConnectionHandlers;
@protocol GNCMConnection;
namespace nearby {
namespace apple {
// A readable stream of bytes.
class BleInputStream : public InputStream {
public:
BleInputStream();
~BleInputStream() override;
// Reads at most `size` bytes from the input stream.
//
// Returns an empty byte array on end of file, or Exception::kIo on error.
ExceptionOr<ByteArray> Read(std::int64_t size) override;
// Closes the stream preventing further reads.
//
// Returns Exception::kIo on error, otherwise Exception::kSuccess.
Exception Close() override;
GNCMConnectionHandlers *GetConnectionHandlers() { return connectionHandlers_; }
private:
GNCMConnectionHandlers *connectionHandlers_;
NSMutableArray<NSData *> *newDataPackets_;
NSMutableData *accumulatedData_;
NSCondition *condition_;
};
// A writable stream of bytes.
class BleOutputStream : public OutputStream {
public:
explicit BleOutputStream(id<GNCMConnection> connection)
: connection_(connection), condition_([[NSCondition alloc] init]) {}
~BleOutputStream() override;
// Write the provided bytes to the output stream.
//
// Returns Exception::kIo on error, otherwise Exception::kSuccess.
Exception Write(const ByteArray &data) override;
// no-op
//
// Always returns Exception::kSuccess.
Exception Flush() override;
// Closes the stream preventing further writes.
//
// Returns Exception::kIo on error, otherwise Exception::kSuccess.
Exception Close() override;
private:
id<GNCMConnection> connection_;
NSCondition *condition_;
};
// A BLE Weave socket.
class BleSocket : public api::ble_v2::BleSocket {
public:
explicit BleSocket(id<GNCMConnection> connection);
// The peripheral used to create the socket must outlive the socket or undefined behavior will
// occur.
BleSocket(id<GNCMConnection> connection, api::ble_v2::BlePeripheral *peripheral);
~BleSocket() override;
// Returns the InputStream of the BleSocket.
// On error, returned stream will report Exception::kIo on any operation.
//
// The returned object is not owned by the caller, and can be invalidated once
// the BleSocket object is destroyed.
BleInputStream &GetInputStream() override { return *input_stream_; }
// Returns the OutputStream of the BleSocket.
// On error, returned stream will report Exception::kIo on any operation.
//
// The returned object is not owned by the caller, and can be invalidated once
// the BleSocket object is destroyed.
BleOutputStream &GetOutputStream() override { return *output_stream_; }
// Returns Exception::kIo on error, otherwise Exception::kSuccess.
Exception Close() override ABSL_LOCKS_EXCLUDED(mutex_);
// Returns valid BlePeripheral pointer if there is a connection, and
// nullptr otherwise.
api::ble_v2::BlePeripheral *GetRemotePeripheral() override { return peripheral_; }
bool IsClosed() const ABSL_LOCKS_EXCLUDED(mutex_);
private:
void DoClose() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
mutable absl::Mutex mutex_;
bool closed_ ABSL_GUARDED_BY(mutex_) = false;
std::unique_ptr<BleInputStream> input_stream_;
std::unique_ptr<BleOutputStream> output_stream_;
api::ble_v2::BlePeripheral *peripheral_;
};
} // namespace apple
} // namespace nearby
@@ -0,0 +1,215 @@
// 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/ble_socket.h"
#include "internal/platform/implementation/ble_v2.h"
#import "internal/platform/implementation/apple/Mediums/Ble/GNCMBleConnection.h"
#import "internal/platform/implementation/apple/ble_peripheral.h"
#import "internal/platform/implementation/apple/ble_utils.h"
#import "internal/platform/implementation/apple/utils.h"
// TODO(b/293336684): Remove this file when shared Weave is complete.
namespace nearby {
namespace apple {
#pragma mark - BleInputStream
BleInputStream::BleInputStream()
: newDataPackets_([NSMutableArray array]),
accumulatedData_([NSMutableData data]),
condition_([[NSCondition alloc] init]) {
// Create the handlers of incoming data from the remote endpoint.
connectionHandlers_ = [GNCMConnectionHandlers
payloadHandler:^(NSData *data) {
[condition_ lock];
// Add the incoming data to the data packet array to be processed in read() below.
[newDataPackets_ addObject:data];
[condition_ broadcast];
[condition_ unlock];
}
disconnectedHandler:^{
[condition_ lock];
// Release the data packet array, meaning the stream has been closed or severed.
newDataPackets_ = nil;
[condition_ broadcast];
[condition_ unlock];
}];
}
BleInputStream::~BleInputStream() {
NSCAssert(!newDataPackets_, @"BleInputStream not closed before destruction");
}
ExceptionOr<ByteArray> BleInputStream::Read(std::int64_t size) {
// Block until either (a) the connection has been closed, (b) we have enough data to return.
NSData *dataToReturn;
[condition_ lock];
while (true) {
// Check if the stream has been closed or severed.
if (!newDataPackets_) break;
if (newDataPackets_.count > 0) {
// Add the packet data to the accumulated data.
for (NSData *data in newDataPackets_) {
if (data.length > 0) {
[accumulatedData_ appendData:data];
}
}
[newDataPackets_ removeAllObjects];
}
if ((size == -1) && (accumulatedData_.length > 0)) {
// Return all of the data.
dataToReturn = accumulatedData_;
accumulatedData_ = [NSMutableData data];
break;
} else if (accumulatedData_.length > 0) {
// Return up to |size| bytes of the data.
std::int64_t sizeToReturn = (accumulatedData_.length < size) ? accumulatedData_.length : size;
NSRange range = NSMakeRange(0, (NSUInteger)sizeToReturn);
dataToReturn = [accumulatedData_ subdataWithRange:range];
[accumulatedData_ replaceBytesInRange:range withBytes:nil length:0];
break;
}
[condition_ wait];
}
[condition_ unlock];
if (dataToReturn) {
NSLog(@"[NEARBY] Input stream: Received data of size: %lu", (unsigned long)dataToReturn.length);
return ExceptionOr<ByteArray>(ByteArrayFromNSData(dataToReturn));
} else {
return ExceptionOr<ByteArray>{Exception::kIo};
}
}
Exception BleInputStream::Close() {
// Unblock pending read operation.
[condition_ lock];
newDataPackets_ = nil;
[condition_ broadcast];
[condition_ unlock];
return {Exception::kSuccess};
}
#pragma mark - BleOutputStream
BleOutputStream::~BleOutputStream() {
NSCAssert(!connection_, @"BleOutputStream not closed before destruction");
}
Exception BleOutputStream::Write(const ByteArray &data) {
[condition_ lock];
NSLog(@"[NEARBY] Sending data of size: %lu", NSDataFromByteArray(data).length);
if (!connection_) {
[condition_ unlock];
return {Exception::kIo};
}
NSMutableData *packet = [NSMutableData dataWithData:NSDataFromByteArray(data)];
// Send the data, blocking until the completion handler is called.
__block bool isComplete = NO;
__block GNCMPayloadResult sendResult = GNCMPayloadResultFailure;
NSCondition *condition = condition_; // don't capture |this| in completion
[connection_ sendData:packet
progressHandler:^(size_t count) {
}
completion:^(GNCMPayloadResult result) {
[condition lock];
// Make sure we haven't already reported completion before. This prevents a crash
// where we try leaving a dispatch group more times than we entered it.
// b/79095653.
if (isComplete) {
[condition unlock];
return;
}
isComplete = YES;
sendResult = result;
[condition broadcast];
[condition unlock];
}];
while (connection_ && !isComplete) {
[condition_ wait];
}
if (sendResult == GNCMPayloadResultSuccess) {
[condition_ unlock];
return {Exception::kSuccess};
} else {
[condition_ unlock];
return {Exception::kIo};
}
}
Exception BleOutputStream::Flush() {
// The write() function blocks until the data is received by the remote endpoint, so there's
// nothing to do here.
return {Exception::kSuccess};
}
Exception BleOutputStream::Close() {
// Unblock pending write operation.
[condition_ lock];
connection_ = nil;
[condition_ broadcast];
[condition_ unlock];
return {Exception::kSuccess};
}
#pragma mark - BleSocket
BleSocket::BleSocket(id<GNCMConnection> connection)
: input_stream_(new BleInputStream()),
output_stream_(new BleOutputStream(connection)),
peripheral_(new EmptyBlePeripheral()) {}
BleSocket::BleSocket(id<GNCMConnection> connection, api::ble_v2::BlePeripheral *peripheral)
: input_stream_(new BleInputStream()),
output_stream_(new BleOutputStream(connection)),
peripheral_(peripheral) {}
BleSocket::~BleSocket() {
absl::MutexLock lock(&mutex_);
DoClose();
}
bool BleSocket::IsClosed() const {
absl::MutexLock lock(&mutex_);
return closed_;
}
Exception BleSocket::Close() {
absl::MutexLock lock(&mutex_);
DoClose();
return {Exception::kSuccess};
}
void BleSocket::DoClose() {
if (!closed_) {
input_stream_->Close();
output_stream_->Close();
closed_ = true;
}
}
} // namespace apple
} // namespace nearby
@@ -56,6 +56,9 @@ CBCharacteristicProperties CBCharacteristicPropertiesFromCPP(
/** Converts a C++ characteristic to an Objective-C characteristic. */
GNCBLEGATTCharacteristic *ObjCGATTCharacteristicFromCPP(const api::ble_v2::GattCharacteristic &c);
NSMutableDictionary<CBUUID *, NSData *> *ObjCServiceDataFromCPP(
const absl::flat_hash_map<Uuid, nearby::ByteArray> &sd);
/** Converts a 16 or 128 bit CoreBluetooth UUID to a C++ UUID. */
Uuid CPPUUIDFromObjC(CBUUID *uuid);
@@ -20,6 +20,7 @@
#include <string>
#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEGATTCharacteristic.h"
#import "internal/platform/implementation/apple/utils.h"
namespace nearby {
namespace apple {
@@ -98,6 +99,17 @@ GNCBLEGATTCharacteristic *ObjCGATTCharacteristicFromCPP(const GattCharacteristic
properties:properties];
}
NSMutableDictionary<CBUUID *, NSData *> *ObjCServiceDataFromCPP(
const absl::flat_hash_map<Uuid, nearby::ByteArray> &sd) {
NSMutableDictionary<CBUUID *, NSData *> *serviceData = [NSMutableDictionary dictionary];
for (const auto &pair : sd) {
CBUUID *key = CBUUID16FromCPP(pair.first);
NSData *data = NSDataFromByteArray(pair.second);
[serviceData setObject:data forKey:key];
}
return serviceData;
}
Uuid CPPUUIDFromObjC(CBUUID *uuid) {
NSString *uuidString = uuid.UUIDString;
if (uuidString.length == 4) {
@@ -1,108 +0,0 @@
// Copyright 2022 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.
#ifndef THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_APPLE_BLUETOOTH_ADAPTER_H_
#define THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_APPLE_BLUETOOTH_ADAPTER_H_
#include <string>
#import "internal/platform/implementation/apple/Mediums/Ble/GNCMBleCentral.h"
#include "internal/platform/implementation/ble_v2.h"
#include "internal/platform/implementation/bluetooth_adapter.h"
#include "internal/platform/prng.h"
namespace nearby {
namespace apple {
class BluetoothAdapter;
// Concrete BlePeripheral implementation.
class BlePeripheral : public api::ble_v2::BlePeripheral {
public:
std::string GetAddress() const override;
api::ble_v2::BlePeripheral::UniqueId GetUniqueId() const override {
return unique_id_;
}
std::string GetPeripheralId() const { return peripheral_id_; }
void SetPeripheralId(const std::string& peripheral_id) {
peripheral_id_ = peripheral_id;
}
void SetConnectionRequester(GNCMBleConnectionRequester connection_requester) {
connection_requester_ = connection_requester;
}
GNCMBleConnectionRequester GetConnectionRequester() {
return connection_requester_;
}
private:
// Only BluetoothAdapter may instantiate BlePeripheral.
friend class BluetoothAdapter;
explicit BlePeripheral(BluetoothAdapter* adapter) : adapter_(*adapter) {
unique_id_ = Prng().NextInt64();
}
BluetoothAdapter& adapter_;
std::string peripheral_id_;
GNCMBleConnectionRequester connection_requester_;
api::ble_v2::BlePeripheral::UniqueId unique_id_;
};
// Concrete BluetoothAdapter implementation.
class BluetoothAdapter : public api::BluetoothAdapter {
public:
using Status = api::BluetoothAdapter::Status;
using ScanMode = api::BluetoothAdapter::ScanMode;
~BluetoothAdapter() override { SetStatus(Status::kDisabled); }
bool SetStatus(Status status) override {
enabled_ = status == Status::kEnabled;
return true;
}
bool IsEnabled() const override { return enabled_; }
ScanMode GetScanMode() const override { return mode_; }
bool SetScanMode(ScanMode mode) override { return false; }
std::string GetName() const override { return name_; }
bool SetName(absl::string_view name) {
return SetName(name, /* persist= */ true);
}
bool SetName(absl::string_view name, bool persist) override {
name_ = std::string(name);
return true;
}
std::string GetMacAddress() const override { return mac_address_; }
void SetMacAddress(absl::string_view mac_address) {
mac_address_ = std::string(mac_address);
}
BlePeripheral& GetPeripheral() { return peripheral_; }
private:
BlePeripheral peripheral_{this};
ScanMode mode_ = ScanMode::kNone;
std::string name_;
std::string mac_address_;
bool enabled_ = true;
};
} // namespace apple
} // namespace nearby
#endif // THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_APPLE_BLUETOOTH_ADAPTER_H_
@@ -0,0 +1,90 @@
// Copyright 2022 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.
// Note: File language is detected using heuristics. Many Objective-C++ headers are incorrectly
// classified as C++ resulting in invalid linter errors. The use of "NSArray" and other Foundation
// classes like "NSData", "NSDictionary" and "NSUUID" are highly weighted for Objective-C and
// Objective-C++ scores. Oddly, "#import <Foundation/Foundation.h>" does not contribute any points.
// This comment alone should be enough to trick the IDE in to believing this is actually some sort
// of Objective-C file. See: cs/google3/devtools/search/lang/recognize_language_classifiers_data
#import <Foundation/Foundation.h>
#include <string>
#include "absl/strings/string_view.h"
#include "internal/platform/implementation/bluetooth_adapter.h"
namespace nearby {
namespace apple {
// Concrete implementation representing the local device Bluetooth adapter.
class BluetoothAdapter : public api::BluetoothAdapter {
public:
BluetoothAdapter() = default;
~BluetoothAdapter() override = default;
// Synchronously sets the status of the BluetoothAdapter to @c status, and
// returns true if the operation was a success.
bool SetStatus(api::BluetoothAdapter::Status status) override;
// Returns true if the BluetoothAdapter's current status is @c kEnabled.
bool IsEnabled() const override;
// Get the current Bluetooth scan mode of the local Bluetooth adapter.
//
// The Bluetooth scan mode determines if the local adapter is connectable
// and/or discoverable from remote Bluetooth devices.
//
// Possible values are: @c kNone, @c kConnectable,
// @c kConnectableDiscoverable.
//
// Returns @c kUnknown on error.
api::BluetoothAdapter::ScanMode GetScanMode() const override;
// Synchronously sets the scan mode of the adapter, and returns true if the
// operation was a success.
bool SetScanMode(api::BluetoothAdapter::ScanMode scan_mode) override;
// Get the friendly Bluetooth name of the local Bluetooth adapter.
//
// Returns an empty string on error
std::string GetName() const override;
// Set and persist the friendly Bluetooth name of the local Bluetooth adapter.
//
// This name is visible to remote Bluetooth devices.
//
// Valid Bluetooth names are a maximum of 248 bytes using UTF-8 encoding,
// although many remote devices can only display the first 40 characters, and
// some may be limited to just 20.
bool SetName(absl::string_view name) override;
// Set the friendly Bluetooth name of the local Bluetooth adapter.
//
// This name is visible to remote Bluetooth devices.
//
// Valid Bluetooth names are a maximum of 248 bytes using UTF-8 encoding,
// although many remote devices can only display the first 40 characters, and
// some may be limited to just 20.
//
// If persist is false, we will restore the original radio names when complete.
bool SetName(absl::string_view name, bool persist) override;
// Returns BT MAC address assigned to this adapter.
std::string GetMacAddress() const override;
};
} // namespace apple
} // namespace nearby
@@ -0,0 +1,71 @@
// Copyright 2022 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/bluetooth_adapter_v2.h"
#import <Foundation/Foundation.h>
#include <string>
#include "absl/strings/string_view.h"
#include "internal/platform/implementation/bluetooth_adapter.h"
namespace nearby {
namespace apple {
bool BluetoothAdapter::SetStatus(api::BluetoothAdapter::Status status) {
// We can't force a radio state in macOS/iOS.
// NOTE: This is supposed to return the success/failure of the change (which should always be
// "fail" for our case). However, usage in the codebase expects this to return with "success" if
// the radio is on, so we just return whether the radio is currently enabled.
return IsEnabled();
}
// TODO(b/290385712): Implement.
bool BluetoothAdapter::IsEnabled() const {
return true;
}
// TODO(b/290385712): Implement.
api::BluetoothAdapter::ScanMode BluetoothAdapter::GetScanMode() const {
return api::BluetoothAdapter::ScanMode::kUnknown;
}
// TODO(b/290385712): Implement.
bool BluetoothAdapter::SetScanMode(api::BluetoothAdapter::ScanMode scan_mode) {
return false;
}
// TODO(b/290385712): Implement.
std::string BluetoothAdapter::GetName() const {
return "";
}
// TODO(b/290385712): Implement.
bool BluetoothAdapter::SetName(absl::string_view name) {
return false;
}
// TODO(b/290385712): Implement.
bool BluetoothAdapter::SetName(absl::string_view name, bool persist) {
return false;
}
// TODO(b/290385712): Implement.
std::string BluetoothAdapter::GetMacAddress() const {
return "";
}
} // namespace apple
} // namespace nearby
@@ -58,6 +58,20 @@ class DeviceInfo : public api::DeviceInfo {
std::function<void(api::DeviceInfo::ScreenStatus)> callback) override;
void UnregisterScreenLockedListener(absl::string_view listener_name) override;
// Request the system to prevent the device from going to sleep.
//
// To allow the system to sleep again, call @c AllowSleep().
//
// Returns @c true on success or if the platform does not allow preventing
// sleep. Returns @c false on any other error.
bool PreventSleep() override;
// Release any requests preventing the device from going to sleep.
//
// Returns @c true on success or if the system is already allowed to sleep.
// Returns @c false if an error occurred.
bool AllowSleep() override;
};
} // namespace apple
@@ -168,5 +168,9 @@ void DeviceInfo::RegisterScreenLockedListener(
void DeviceInfo::UnregisterScreenLockedListener(absl::string_view listener_name) {}
bool DeviceInfo::PreventSleep() { return true; }
bool DeviceInfo::AllowSleep() { return true; }
} // namespace apple
} // namespace nearby
@@ -44,37 +44,45 @@ namespace api {
std::string ImplementationPlatform::GetCustomSavePath(const std::string& parent_folder,
const std::string& file_name) {
NSFileManager* manager = [NSFileManager defaultManager];
// Collapse any path escaping characters.
NSString* parentFolder = [@(parent_folder.c_str()) stringByReplacingOccurrencesOfString:@"../"
withString:@""];
NSURL* parentFolderURL = [NSURL fileURLWithPath:parentFolder];
NSURL* parentFolder = [NSURL fileURLWithPath:@(parent_folder.c_str())];
NSString* fileName = @(file_name.c_str());
// The only reserved character in a file name on macOS is the forward-slash. It's unclear if iOS
// has any additional restrictions.
//
// """
// In the Finder, filenames containing `/` can be created, but `/` is stored as a colon (:) in the
// filesystem, and is shown as such on the command line. Filenames containing `:` created from the
// command line are shown with `/` instead of `:` in the Finder, so that it is impossible to
// create a file that the Finder shows as having a `:` in its filename.
// """
//
// See: https://en.wikipedia.org/wiki/Filename
NSString* fileName = [@(file_name.c_str()) stringByReplacingOccurrencesOfString:@"/"
withString:@":"];
NSString* baseName = [fileName stringByDeletingPathExtension];
NSString* extension = [fileName pathExtension];
NSURL* url = [parentFolder URLByAppendingPathComponent:fileName];
NSURL* url = [parentFolderURL URLByAppendingPathComponent:fileName];
NSInteger index = 1;
while ([manager fileExistsAtPath:url.path]) {
while ([NSFileManager.defaultManager fileExistsAtPath:url.path]) {
index++;
NSString* fileName =
[NSString stringWithFormat:@"%@ %@.%@", baseName, [@(index) stringValue], extension];
url = [parentFolder URLByAppendingPathComponent:fileName];
url = [parentFolderURL URLByAppendingPathComponent:fileName];
}
return [url.path UTF8String];
return url.path.UTF8String;
}
std::string ImplementationPlatform::GetDownloadPath(const std::string& parent_folder,
const std::string& file_name) {
// TODO(jfcarroll): This needs to be done correctly, we now have a file name and parent folder,
// they should be combined with the default download path
NSString* fileName = ObjCStringFromCppString(file_name);
// TODO(b/227535777): If file name matches an existing file, it will be overwritten. Append a
// number until a unique file name is reached 'foobar (2).png'.
return CppStringFromObjCString([NSTemporaryDirectory() stringByAppendingPathComponent:fileName]);
NSString* customSavePath =
[NSTemporaryDirectory() stringByAppendingPathComponent:@(parent_folder.c_str())];
return GetCustomSavePath(customSavePath.UTF8String, file_name);
}
OSName ImplementationPlatform::GetCurrentOS() { return OSName::kApple; }
@@ -162,7 +170,7 @@ std::unique_ptr<BleMedium> ImplementationPlatform::CreateBleMedium(api::Bluetoot
std::unique_ptr<ble_v2::BleMedium> ImplementationPlatform::CreateBleV2Medium(
api::BluetoothAdapter& adapter) {
return std::make_unique<apple::BleMedium>(adapter);
return std::make_unique<apple::BleMedium>();
}
std::unique_ptr<ServerSyncMedium> ImplementationPlatform::CreateServerSyncMedium() {
+2 -4
View File
@@ -99,10 +99,8 @@ class BleMedium {
virtual bool StopScanning(const std::string& service_id) = 0;
// Callback that is invoked when a new connection is accepted.
struct AcceptedConnectionCallback {
absl::AnyInvocable<void(BleSocket& socket, const std::string& service_id)>
accepted_cb = DefaultCallback<BleSocket&, const std::string&>();
};
using AcceptedConnectionCallback = absl::AnyInvocable<void(
BleSocket& socket, const std::string& service_id)>;
// Returns true once BLE socket connection requests to service_id can be
// accepted.
@@ -92,6 +92,8 @@ class BlePeripheral {
// https://developer.android.com/reference/android/bluetooth/BluetoothDevice#getAddress()
//
// Returns the current address.
//
// This will always be an empty string on Apple platforms.
virtual std::string GetAddress() const = 0;
// Returns an immutable unique identifier. The identifier must not change when
@@ -510,6 +512,9 @@ class BleMedium {
// Calls `callback` and returns true if `mac_address` is a valid BLE address.
// Otherwise, does not call the callback and returns false.
//
// This method is not available on Apple platforms and will always return
// false, ignoring the callback.
virtual bool GetRemotePeripheral(const std::string& mac_address,
GetRemotePeripheralCallback callback) = 0;
@@ -59,6 +59,10 @@ class DeviceInfo {
std::function<void(ScreenStatus)> callback) = 0;
virtual void UnregisterScreenLockedListener(
absl::string_view listener_name) = 0;
// Control device sleep
virtual bool PreventSleep() = 0;
virtual bool AllowSleep() = 0;
};
} // namespace api
+5 -3
View File
@@ -30,7 +30,6 @@ cc_library(
"log_message.h",
"multi_thread_executor.h",
"mutex.h",
"pipe.h",
"preferences_manager.h",
"scheduled_executor.h",
"single_thread_executor.h",
@@ -44,8 +43,8 @@ cc_library(
deps = [
":preferences_repository",
"//internal/platform:base",
"//internal/platform:logging",
"//internal/platform:test_util",
"//internal/platform:types",
"//internal/platform:util",
"//internal/platform/implementation:types",
"//internal/platform/implementation/shared:count_down_latch",
@@ -85,6 +84,7 @@ cc_library(
"bluetooth_adapter.h",
"bluetooth_classic.h",
"credential_storage_impl.h",
"socket_base.h",
"webrtc.h",
"wifi.h",
"wifi_direct.h",
@@ -97,13 +97,15 @@ cc_library(
"@com_google_absl//absl/base:core_headers",
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/container:flat_hash_set",
"@com_google_absl//absl/functional:any_invocable",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/synchronization",
"//internal/platform:base",
"//internal/platform:cancellation_flag",
"//internal/platform:logging",
"//internal/platform:test_util",
"//internal/platform:types",
"//internal/platform:uuid",
+8 -65
View File
@@ -17,10 +17,18 @@
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include "absl/functional/any_invocable.h"
#include "absl/log/check.h"
#include "absl/synchronization/mutex.h"
#include "internal/platform/byte_array.h"
#include "internal/platform/cancellation_flag.h"
#include "internal/platform/cancellation_flag_listener.h"
#include "internal/platform/exception.h"
#include "internal/platform/implementation/ble.h"
#include "internal/platform/implementation/bluetooth_adapter.h"
#include "internal/platform/implementation/g3/bluetooth_adapter.h"
#include "internal/platform/implementation/shared/count_down_latch.h"
#include "internal/platform/logging.h"
#include "internal/platform/medium_environment.h"
@@ -28,76 +36,11 @@
namespace nearby {
namespace g3 {
BleSocket::~BleSocket() {
absl::MutexLock lock(&mutex_);
DoClose();
}
void BleSocket::Connect(BleSocket& other) {
absl::MutexLock lock(&mutex_);
remote_socket_ = &other;
input_ = other.output_;
}
InputStream& BleSocket::GetInputStream() {
auto* remote_socket = GetRemoteSocket();
CHECK(remote_socket != nullptr);
return remote_socket->GetLocalInputStream();
}
OutputStream& BleSocket::GetOutputStream() { return GetLocalOutputStream(); }
BleSocket* BleSocket::GetRemoteSocket() {
absl::MutexLock lock(&mutex_);
return remote_socket_;
}
bool BleSocket::IsConnected() const {
absl::MutexLock lock(&mutex_);
return IsConnectedLocked();
}
bool BleSocket::IsClosed() const {
absl::MutexLock lock(&mutex_);
return closed_;
}
Exception BleSocket::Close() {
absl::MutexLock lock(&mutex_);
DoClose();
return {Exception::kSuccess};
}
BlePeripheral* BleSocket::GetRemotePeripheral() {
absl::MutexLock lock(&mutex_);
return peripheral_;
}
void BleSocket::DoClose() {
if (!closed_) {
remote_socket_ = nullptr;
output_->GetOutputStream().Close();
output_->GetInputStream().Close();
if (IsConnectedLocked()) {
input_->GetOutputStream().Close();
input_->GetInputStream().Close();
}
closed_ = true;
}
}
bool BleSocket::IsConnectedLocked() const { return input_ != nullptr; }
InputStream& BleSocket::GetLocalInputStream() {
absl::MutexLock lock(&mutex_);
return output_->GetInputStream();
}
OutputStream& BleSocket::GetLocalOutputStream() {
absl::MutexLock lock(&mutex_);
return output_->GetOutputStream();
}
std::unique_ptr<api::BleSocket> BleServerSocket::Accept(
BlePeripheral* peripheral) {
absl::MutexLock lock(&mutex_);
+12 -40
View File
@@ -27,7 +27,7 @@
#include "internal/platform/implementation/g3/bluetooth_adapter.h"
#include "internal/platform/implementation/g3/bluetooth_classic.h"
#include "internal/platform/implementation/g3/multi_thread_executor.h"
#include "internal/platform/implementation/g3/pipe.h"
#include "internal/platform/implementation/g3/socket_base.h"
#include "internal/platform/input_stream.h"
#include "internal/platform/output_stream.h"
@@ -36,64 +36,36 @@ namespace g3 {
class BleMedium;
class BleSocket : public api::BleSocket {
class BleSocket : public api::BleSocket, public SocketBase {
public:
BleSocket() = default;
explicit BleSocket(BlePeripheral* peripheral) : peripheral_(peripheral) {}
~BleSocket() override;
// Connect to another BleSocket, to form a functional low-level channel.
// from this point on, and until Close is called, connection exists.
void Connect(BleSocket& other) ABSL_LOCKS_EXCLUDED(mutex_);
// Returns the InputStream of this connected BleSocket.
InputStream& GetInputStream() override ABSL_LOCKS_EXCLUDED(mutex_);
InputStream& GetInputStream() override {
return SocketBase::GetInputStream();
}
// Returns the OutputStream of this connected BleSocket.
// This stream is for local side to write.
OutputStream& GetOutputStream() override ABSL_LOCKS_EXCLUDED(mutex_);
OutputStream& GetOutputStream() override {
return SocketBase::GetOutputStream();
}
// Returns address of a remote BleSocket or nullptr.
BleSocket* GetRemoteSocket() ABSL_LOCKS_EXCLUDED(mutex_);
// Returns true if connection exists to the (possibly closed) remote socket.
bool IsConnected() const ABSL_LOCKS_EXCLUDED(mutex_);
// Returns true if socket is closed.
bool IsClosed() const ABSL_LOCKS_EXCLUDED(mutex_);
BleSocket* GetRemoteSocket() {
return static_cast<BleSocket*>(SocketBase::GetRemoteSocket());
}
// Returns Exception::kIo on error, Exception::kSuccess otherwise.
Exception Close() override ABSL_LOCKS_EXCLUDED(mutex_);
Exception Close() override { return SocketBase::Close(); }
// Returns valid BlePeripheral pointer if there is a connection, and
// nullptr otherwise.
BlePeripheral* GetRemotePeripheral() override ABSL_LOCKS_EXCLUDED(mutex_);
private:
void DoClose() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
// Returns true if connection exists to the (possibly closed) remote socket.
bool IsConnectedLocked() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
// Returns InputStream of our side of a connection.
// This is what the remote side is supposed to read from.
// This is a helper for GetInputStream() method.
InputStream& GetLocalInputStream() ABSL_LOCKS_EXCLUDED(mutex_);
// Returns OutputStream of our side of a connection.
// This is what the local size is supposed to write to.
// This is a helper for GetOutputStream() method.
OutputStream& GetLocalOutputStream() ABSL_LOCKS_EXCLUDED(mutex_);
// Output pipe is initialized by constructor, it remains always valid, until
// it is closed. it represents output part of a local socket. Input part of a
// local socket comes from the peer socket, after connection.
std::shared_ptr<Pipe> output_{new Pipe};
std::shared_ptr<Pipe> input_;
mutable absl::Mutex mutex_;
BlePeripheral* peripheral_;
BleSocket* remote_socket_ ABSL_GUARDED_BY(mutex_) = nullptr;
bool closed_ ABSL_GUARDED_BY(mutex_) = false;
};
class BleServerSocket {
+16 -74
View File
@@ -15,7 +15,6 @@
#include "internal/platform/implementation/g3/ble_v2.h"
#include <algorithm>
#include <cstdint>
#include <iostream>
#include <memory>
#include <optional>
@@ -23,15 +22,26 @@
#include <utility>
#include <vector>
#include "absl/functional/any_invocable.h"
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/escaping.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/mutex.h"
#include "internal/platform/borrowable.h"
#include "internal/platform/byte_array.h"
#include "internal/platform/cancellation_flag_listener.h"
#include "internal/platform/count_down_latch.h"
#include "internal/platform/exception.h"
#include "internal/platform/implementation/ble_v2.h"
#include "internal/platform/implementation/bluetooth_adapter.h"
#include "internal/platform/implementation/g3/bluetooth_adapter.h"
#include "internal/platform/logging.h"
#include "internal/platform/medium_environment.h"
#include "internal/platform/prng.h"
#include "internal/platform/uuid.h"
namespace nearby {
namespace g3 {
@@ -69,82 +79,14 @@ api::ble_v2::BlePeripheral::UniqueId BleV2Peripheral::GetUniqueId() const {
return adapter_.GetUniqueId();
}
BleV2Socket::~BleV2Socket() {
absl::MutexLock lock(&mutex_);
DoClose();
}
void BleV2Socket::Connect(BleV2Socket& other) {
absl::MutexLock lock(&mutex_);
remote_socket_ = &other;
input_ = other.output_;
}
InputStream& BleV2Socket::GetInputStream() {
auto* remote_socket = GetRemoteSocket();
CHECK(remote_socket != nullptr);
return remote_socket->GetLocalInputStream();
}
OutputStream& BleV2Socket::GetOutputStream() { return GetLocalOutputStream(); }
BleV2Socket* BleV2Socket::GetRemoteSocket() {
absl::MutexLock lock(&mutex_);
return remote_socket_;
}
bool BleV2Socket::IsConnected() const {
absl::MutexLock lock(&mutex_);
return IsConnectedLocked();
}
bool BleV2Socket::IsClosed() const {
absl::MutexLock lock(&mutex_);
return closed_;
}
Exception BleV2Socket::Close() {
absl::MutexLock lock(&mutex_);
DoClose();
return {Exception::kSuccess};
}
BleV2Peripheral* BleV2Socket::GetRemotePeripheral() {
BluetoothAdapter* remote_adapter = nullptr;
{
absl::MutexLock lock(&mutex_);
if (remote_socket_ == nullptr || remote_socket_->adapter_ == nullptr) {
return nullptr;
}
remote_adapter = remote_socket_->adapter_;
}
if (remote_adapter == nullptr || remote_adapter->GetBleV2Medium() == nullptr)
BleV2Socket* remote_socket = GetRemoteSocket();
if (remote_socket == nullptr || remote_socket->adapter_ == nullptr ||
remote_socket->adapter_->GetBleV2Medium() == nullptr) {
return nullptr;
return &(static_cast<BleV2Medium*>(remote_adapter->GetBleV2Medium())
->GetPeripheral());
}
void BleV2Socket::DoClose() {
if (!closed_) {
remote_socket_ = nullptr;
output_->GetOutputStream().Close();
output_->GetInputStream().Close();
input_->GetOutputStream().Close();
input_->GetInputStream().Close();
closed_ = true;
}
}
bool BleV2Socket::IsConnectedLocked() const { return input_ != nullptr; }
InputStream& BleV2Socket::GetLocalInputStream() {
absl::MutexLock lock(&mutex_);
return output_->GetInputStream();
}
OutputStream& BleV2Socket::GetLocalOutputStream() {
absl::MutexLock lock(&mutex_);
return output_->GetOutputStream();
return &(static_cast<BleV2Medium*>(remote_socket->adapter_->GetBleV2Medium())
->GetPeripheral());
}
std::unique_ptr<api::ble_v2::BleSocket> BleV2ServerSocket::Accept() {
+12 -44
View File
@@ -29,7 +29,7 @@
#include "internal/platform/byte_array.h"
#include "internal/platform/implementation/ble_v2.h"
#include "internal/platform/implementation/g3/bluetooth_adapter.h"
#include "internal/platform/implementation/g3/pipe.h"
#include "internal/platform/implementation/g3/socket_base.h"
#include "internal/platform/medium_environment.h"
#include "internal/platform/prng.h"
#include "internal/platform/uuid.h"
@@ -50,67 +50,35 @@ class BleV2Peripheral : public api::ble_v2::BlePeripheral {
BluetoothAdapter& adapter_;
};
class BleV2Socket : public api::ble_v2::BleSocket {
class BleV2Socket : public api::ble_v2::BleSocket, public SocketBase {
public:
explicit BleV2Socket(BluetoothAdapter* adapter) : adapter_(adapter) {}
BleV2Socket(const BleV2Socket&) = default;
BleV2Socket& operator=(const BleV2Socket&) = default;
BleV2Socket(BleV2Socket&&) = default;
BleV2Socket& operator=(BleV2Socket&&) = default;
~BleV2Socket() override;
// Connect to another BleSocket, to form a functional low-level channel.
// from this point on, and until Close is called, connection exists.
void Connect(BleV2Socket& other) ABSL_LOCKS_EXCLUDED(mutex_);
// Returns the InputStream of this connected BleSocket.
InputStream& GetInputStream() override ABSL_LOCKS_EXCLUDED(mutex_);
InputStream& GetInputStream() override {
return SocketBase::GetInputStream();
}
// Returns the OutputStream of this connected BleSocket.
// This stream is for local side to write.
OutputStream& GetOutputStream() override ABSL_LOCKS_EXCLUDED(mutex_);
OutputStream& GetOutputStream() override {
return SocketBase::GetOutputStream();
}
// Returns address of a remote BleSocket or nullptr.
BleV2Socket* GetRemoteSocket() ABSL_LOCKS_EXCLUDED(mutex_);
// Returns true if connection exists to the (possibly closed) remote socket.
bool IsConnected() const ABSL_LOCKS_EXCLUDED(mutex_);
// Returns true if socket is closed.
bool IsClosed() const ABSL_LOCKS_EXCLUDED(mutex_);
BleV2Socket* GetRemoteSocket() {
return static_cast<BleV2Socket*>(SocketBase::GetRemoteSocket());
}
// Returns Exception::kIo on error, Exception::kSuccess otherwise.
Exception Close() override ABSL_LOCKS_EXCLUDED(mutex_);
Exception Close() override { return SocketBase::Close(); }
// Returns valid BlePeripheral pointer if there is a connection, and
// nullptr otherwise.
BleV2Peripheral* GetRemotePeripheral() override ABSL_LOCKS_EXCLUDED(mutex_);
private:
void DoClose() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
// Returns true if connection exists to the (possibly closed) remote socket.
bool IsConnectedLocked() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
// Returns InputStream of our side of a connection.
// This is what the remote side is supposed to read from.
// This is a helper for GetInputStream() method.
InputStream& GetLocalInputStream() ABSL_LOCKS_EXCLUDED(mutex_);
// Returns OutputStream of our side of a connection.
// This is what the local size is supposed to write to.
// This is a helper for GetOutputStream() method.
OutputStream& GetLocalOutputStream() ABSL_LOCKS_EXCLUDED(mutex_);
// Output pipe is initialized by constructor, it remains always valid, until
// it is closed. it represents output part of a local socket. Input part of a
// local socket comes from the peer socket, after connection.
std::shared_ptr<Pipe> output_{new Pipe};
std::shared_ptr<Pipe> input_;
mutable absl::Mutex mutex_;
BluetoothAdapter* adapter_ = nullptr; // Our Adapter. Read only.
BleV2Socket* remote_socket_ ABSL_GUARDED_BY(mutex_) = nullptr;
bool closed_ ABSL_GUARDED_BY(mutex_) = false;
};
class BleV2ServerSocket : public api::ble_v2::BleServerSocket {
@@ -17,92 +17,31 @@
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "absl/functional/any_invocable.h"
#include "absl/log/check.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/mutex.h"
#include "internal/platform/cancellation_flag.h"
#include "internal/platform/cancellation_flag_listener.h"
#include "internal/platform/exception.h"
#include "internal/platform/implementation/bluetooth_adapter.h"
#include "internal/platform/implementation/bluetooth_classic.h"
#include "internal/platform/implementation/g3/bluetooth_adapter.h"
#include "internal/platform/input_stream.h"
#include "internal/platform/logging.h"
#include "internal/platform/medium_environment.h"
namespace nearby {
namespace g3 {
BluetoothSocket::~BluetoothSocket() {
absl::MutexLock lock(&mutex_);
DoClose();
}
void BluetoothSocket::Connect(BluetoothSocket& other) {
absl::MutexLock lock(&mutex_);
remote_socket_ = &other;
input_ = other.output_;
}
bool BluetoothSocket::IsConnected() const {
absl::MutexLock lock(&mutex_);
return IsConnectedLocked();
}
bool BluetoothSocket::IsClosed() const {
absl::MutexLock lock(&mutex_);
return closed_;
}
bool BluetoothSocket::IsConnectedLocked() const { return input_ != nullptr; }
InputStream& BluetoothSocket::GetInputStream() {
absl::MutexLock lock(&mutex_);
if (IsConnectedLocked()) {
return input_->GetInputStream();
} else {
return invalid_input_stream_;
}
}
OutputStream& BluetoothSocket::GetOutputStream() {
return GetLocalOutputStream();
}
InputStream& BluetoothSocket::GetLocalInputStream() {
absl::MutexLock lock(&mutex_);
return output_->GetInputStream();
}
OutputStream& BluetoothSocket::GetLocalOutputStream() {
absl::MutexLock lock(&mutex_);
return output_->GetOutputStream();
}
Exception BluetoothSocket::Close() {
absl::MutexLock lock(&mutex_);
DoClose();
return {Exception::kSuccess};
}
void BluetoothSocket::DoClose() {
if (!closed_) {
remote_socket_ = nullptr;
output_->GetOutputStream().Close();
output_->GetInputStream().Close();
input_->GetOutputStream().Close();
input_->GetInputStream().Close();
input_.reset();
closed_ = true;
}
}
BluetoothDevice* BluetoothSocket::GetRemoteDevice() {
BluetoothAdapter* remote_adapter = nullptr;
{
absl::MutexLock lock(&mutex_);
if (remote_socket_ == nullptr || remote_socket_->adapter_ == nullptr) {
return nullptr;
}
remote_adapter = remote_socket_->adapter_;
BluetoothSocket* remote_socket =
static_cast<BluetoothSocket*>(GetRemoteSocket());
if (remote_socket == nullptr || remote_socket->adapter_ == nullptr) {
return nullptr;
}
return remote_adapter ? &remote_adapter->GetDevice() : nullptr;
return &remote_socket->adapter_->GetDevice();
}
std::unique_ptr<api::BluetoothSocket> BluetoothServerSocket::Accept() {
@@ -26,7 +26,7 @@
#include "internal/platform/exception.h"
#include "internal/platform/implementation/bluetooth_classic.h"
#include "internal/platform/implementation/g3/bluetooth_adapter.h"
#include "internal/platform/implementation/g3/pipe.h"
#include "internal/platform/implementation/g3/socket_base.h"
#include "internal/platform/input_stream.h"
#include "internal/platform/listeners.h"
#include "internal/platform/output_stream.h"
@@ -35,83 +35,34 @@ namespace nearby {
namespace g3 {
// https://developer.android.com/reference/android/bluetooth/BluetoothSocket.html.
class BluetoothSocket : public api::BluetoothSocket {
class BluetoothSocket : public api::BluetoothSocket, public SocketBase {
public:
BluetoothSocket() = default;
explicit BluetoothSocket(BluetoothAdapter* adapter) : adapter_(adapter) {}
~BluetoothSocket() override;
// Connects to another BluetoothSocket, to form a functional low-level
// channel. From this point on, and until Close is called, connection exists.
void Connect(BluetoothSocket& other);
// NOTE:
// It is an undefined behavior if GetInputStream() or GetOutputStream() is
// called for a not-connected BluetoothSocket, i.e. any object that is not
// returned by BluetoothClassicMedium::ConnectToService() for client side or
// BluetoothServerSocket::Accept() for server side of connection.
// Returns the InputStream of this connected BluetoothSocket.
InputStream& GetInputStream() override;
InputStream& GetInputStream() override {
return SocketBase::GetInputStream();
}
// Returns the OutputStream of this connected BluetoothSocket.
// This stream is for local side to write.
OutputStream& GetOutputStream() override;
// Returns true if connection exists to the (possibly closed) remote socket.
bool IsConnected() const ABSL_LOCKS_EXCLUDED(mutex_);
// Returns true if socket is closed.
bool IsClosed() const ABSL_LOCKS_EXCLUDED(mutex_);
OutputStream& GetOutputStream() override {
return SocketBase::GetOutputStream();
}
// Closes both input and output streams, marks Socket as closed.
// After this call object should be treated as not connected.
// Returns Exception::kIo on error, Exception::kSuccess otherwise.
Exception Close() override ABSL_LOCKS_EXCLUDED(mutex_);
Exception Close() override { return SocketBase::Close(); }
// https://developer.android.com/reference/android/bluetooth/BluetoothSocket.html#getRemoteDevice()
// Returns valid BluetoothDevice pointer if there is a connection, and
// nullptr otherwise.
BluetoothDevice* GetRemoteDevice() override ABSL_LOCKS_EXCLUDED(mutex_);
BluetoothDevice* GetRemoteDevice() override;
private:
void DoClose() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
// Returns true if connection exists to the (possibly closed) remote socket.
bool IsConnectedLocked() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
// Returns InputStream of our side of a connection.
// This is what the remote side is supposed to read from.
// This is a helper for GetInputStream() method.
InputStream& GetLocalInputStream() ABSL_LOCKS_EXCLUDED(mutex_);
// Returns OutputStream of our side of a connection.
// This is what the local size is supposed to write to.
// This is a helper for GetOutputStream() method.
OutputStream& GetLocalOutputStream() ABSL_LOCKS_EXCLUDED(mutex_);
class InvalidInputStream : public InputStream {
public:
ExceptionOr<ByteArray> Read(std::int64_t size) override {
return ExceptionOr<ByteArray>(Exception::kIo);
}
ExceptionOr<size_t> Skip(size_t offset) override {
return ExceptionOr<size_t>(Exception::kIo);
}
Exception Close() override { return {Exception::kIo}; }
};
// Returned to the caller if the remote socket is destroyed.
InvalidInputStream invalid_input_stream_;
// Output pipe is initialized by constructor, it remains always valid, until
// it is closed. it represents output part of a local socket. Input part of a
// local socket comes from the peer socket, after connection.
std::shared_ptr<Pipe> output_{new Pipe};
std::shared_ptr<Pipe> input_;
mutable absl::Mutex mutex_;
BluetoothAdapter* adapter_ = nullptr; // Our Adapter. Read only.
BluetoothSocket* remote_socket_ ABSL_GUARDED_BY(mutex_) = nullptr;
bool closed_ ABSL_GUARDED_BY(mutex_) = false;
};
// https://developer.android.com/reference/android/bluetooth/BluetoothServerSocket.html.
@@ -38,7 +38,7 @@ class DeviceInfo : public api::DeviceInfo {
return api::DeviceInfo::DeviceType::kLaptop;
}
api::DeviceInfo::OsType GetOsType() const override{
api::DeviceInfo::OsType GetOsType() const override {
return api::DeviceInfo::OsType::kChromeOs;
}
@@ -92,6 +92,10 @@ class DeviceInfo : public api::DeviceInfo {
screen_locked_listeners_.erase(listener_name);
}
bool PreventSleep() override { return true; }
bool AllowSleep() override { return true; }
private:
absl::flat_hash_map<std::string,
std::function<void(api::DeviceInfo::ScreenStatus)>>
@@ -1,42 +0,0 @@
// Copyright 2020 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.
#ifndef PLATFORM_IMPL_G3_PIPE_H_
#define PLATFORM_IMPL_G3_PIPE_H_
#include <memory>
#include "internal/platform/base_pipe.h"
#include "internal/platform/implementation/g3/condition_variable.h"
#include "internal/platform/implementation/g3/mutex.h"
namespace nearby {
namespace g3 {
class Pipe : public BasePipe {
public:
Pipe() {
auto mutex = std::make_unique<g3::Mutex>(/*check=*/true);
auto cond = std::make_unique<g3::ConditionVariable>(mutex.get());
Setup(std::move(mutex), std::move(cond));
}
~Pipe() override = default;
Pipe(Pipe&&) = delete;
Pipe& operator=(Pipe&&) = delete;
};
} // namespace g3
} // namespace nearby
#endif // PLATFORM_IMPL_G3_PIPE_H_
@@ -0,0 +1,156 @@
// 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.
#ifndef THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_G3_SOCKET_BASE_H_
#define THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_G3_SOCKET_BASE_H_
#include <cstddef>
#include <cstdint>
#include <memory>
#include <tuple>
#include <utility>
#include "absl/base/thread_annotations.h"
#include "absl/synchronization/mutex.h"
#include "internal/platform/byte_array.h"
#include "internal/platform/exception.h"
#include "internal/platform/input_stream.h"
#include "internal/platform/output_stream.h"
#include "internal/platform/pipe.h"
namespace nearby {
namespace g3 {
// Common base for BT, BLE and Wifi socket implementations.
class SocketBase {
public:
SocketBase() { std::tie(input_for_remote_, output_) = CreatePipe(); }
virtual ~SocketBase() {
absl::MutexLock lock(&mutex_);
DoClose();
}
// Connects to another Socket, to form a functional low-level
// channel. From this point on, and until Close is called, connection exists.
void Connect(SocketBase& other) ABSL_LOCKS_EXCLUDED(mutex_) {
absl::MutexLock lock(&mutex_);
remote_socket_ = &other;
input_ = std::move(other.input_for_remote_);
}
// Returns the InputStream of this connected socket.
InputStream& GetInputStream() { return input_proxy_; }
// Returns the OutputStream of this connected socket.
// This stream is for local side to write.
OutputStream& GetOutputStream() ABSL_LOCKS_EXCLUDED(mutex_) {
absl::MutexLock lock(&mutex_);
return *output_;
}
// Returns true if connection exists to the (possibly closed) remote socket.
bool IsConnected() const ABSL_LOCKS_EXCLUDED(mutex_) {
absl::MutexLock lock(&mutex_);
return IsConnectedLocked();
}
// Returns true if socket is closed.
bool IsClosed() const ABSL_LOCKS_EXCLUDED(mutex_) {
absl::MutexLock lock(&mutex_);
return closed_;
}
// Closes both input and output streams, marks Socket as closed.
// After this call object should be treated as not connected.
// Returns Exception::kIo on error, Exception::kSuccess otherwise.
Exception Close() ABSL_LOCKS_EXCLUDED(mutex_) {
absl::MutexLock lock(&mutex_);
DoClose();
return {Exception::kSuccess};
}
SocketBase* GetRemoteSocket() ABSL_LOCKS_EXCLUDED(mutex_) {
absl::MutexLock lock(&mutex_);
return remote_socket_;
}
protected:
mutable absl::Mutex mutex_;
private:
void DoClose() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_) {
if (!closed_) {
remote_socket_ = nullptr;
// The client can hold references to `output_` and `input_` streams. We
// can close them but we cannot destroy them.
output_->Close();
if (input_) {
input_->Close();
}
// The client does not hold a reference to `input_for_remote_`, so we can
// destroy it. Connecting to this socket will fail after that.
input_for_remote_.reset();
closed_ = true;
}
}
// Returns true if connection exists to the (possibly closed) remote socket.
bool IsConnectedLocked() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_) {
return input_ != nullptr;
}
class InputProxyStream : public InputStream {
public:
explicit InputProxyStream(SocketBase* socket) : socket_(socket) {}
ExceptionOr<ByteArray> Read(std::int64_t size) override {
if (!socket_->IsConnected()) {
return ExceptionOr<ByteArray>(Exception::kIo);
}
return socket_->input_->Read(size);
}
ExceptionOr<size_t> Skip(size_t offset) override {
if (!socket_->IsConnected()) {
return ExceptionOr<size_t>(Exception::kIo);
}
return socket_->input_->Skip(offset);
}
Exception Close() override {
if (!socket_->IsConnected()) {
return {Exception::kIo};
}
return socket_->input_->Close();
}
private:
SocketBase* socket_;
};
InputProxyStream input_proxy_{this};
// Output stream is initialized by constructor, it remains always valid. It
// represents output part of a local socket. Input stream of a local socket
// comes from the peer socket, after connection.
std::unique_ptr<OutputStream> output_;
std::unique_ptr<InputStream> input_;
// `input_for_remote_` is the other end of the pipe formed with `output_`. We
// give this stream to the remote socket when they connect to us, and it
// becomes their `input_` stream.
std::unique_ptr<InputStream> input_for_remote_;
SocketBase* remote_socket_ ABSL_GUARDED_BY(mutex_) = nullptr;
bool closed_ ABSL_GUARDED_BY(mutex_) = false;
};
} // namespace g3
} // namespace nearby
#endif // THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_G3_SOCKET_BASE_H_
@@ -16,80 +16,25 @@
#include <iostream>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "absl/log/check.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/mutex.h"
#include "internal/platform/cancellation_flag.h"
#include "internal/platform/exception.h"
#include "internal/platform/implementation/wifi_direct.h"
#include "internal/platform/logging.h"
#include "internal/platform/medium_environment.h"
#include "internal/platform/prng.h"
#include "internal/platform/wifi_credential.h"
namespace nearby {
namespace g3 {
// Code for WifiDirectSocket
WifiDirectSocket::~WifiDirectSocket() {
absl::MutexLock lock(&mutex_);
DoClose();
}
void WifiDirectSocket::Connect(WifiDirectSocket& other) {
absl::MutexLock lock(&mutex_);
remote_socket_ = &other;
input_ = other.output_;
}
InputStream& WifiDirectSocket::GetInputStream() {
auto* remote_socket = GetRemoteSocket();
CHECK(remote_socket != nullptr);
return remote_socket->GetLocalInputStream();
}
OutputStream& WifiDirectSocket::GetOutputStream() {
return GetLocalOutputStream();
}
WifiDirectSocket* WifiDirectSocket::GetRemoteSocket() {
absl::MutexLock lock(&mutex_);
return remote_socket_;
}
bool WifiDirectSocket::IsConnected() const {
absl::MutexLock lock(&mutex_);
return IsConnectedLocked();
}
Exception WifiDirectSocket::Close() {
absl::MutexLock lock(&mutex_);
DoClose();
return {Exception::kSuccess};
}
void WifiDirectSocket::DoClose() {
if (!closed_) {
remote_socket_ = nullptr;
output_->GetOutputStream().Close();
output_->GetInputStream().Close();
input_->GetOutputStream().Close();
input_->GetInputStream().Close();
closed_ = true;
}
}
bool WifiDirectSocket::IsConnectedLocked() const { return input_ != nullptr; }
InputStream& WifiDirectSocket::GetLocalInputStream() {
absl::MutexLock lock(&mutex_);
return output_->GetInputStream();
}
OutputStream& WifiDirectSocket::GetLocalOutputStream() {
absl::MutexLock lock(&mutex_);
return output_->GetOutputStream();
}
// Code for WifiDirectServerSocket
std::string WifiDirectServerSocket::GetName(absl::string_view ip_address,
int port) {
@@ -22,7 +22,7 @@
#include "absl/synchronization/mutex.h"
#include "internal/platform/implementation/g3/multi_thread_executor.h"
#include "internal/platform/implementation/g3/pipe.h"
#include "internal/platform/implementation/g3/socket_base.h"
#include "internal/platform/implementation/wifi_direct.h"
#include "internal/platform/input_stream.h"
#include "internal/platform/output_stream.h"
@@ -32,66 +32,28 @@ namespace g3 {
class WifiDirectMedium;
class WifiDirectSocket : public api::WifiDirectSocket {
class WifiDirectSocket : public api::WifiDirectSocket, public SocketBase {
public:
WifiDirectSocket() = default;
~WifiDirectSocket() override;
WifiDirectSocket(const WifiDirectSocket&) = default;
WifiDirectSocket(WifiDirectSocket&&) = default;
WifiDirectSocket& operator=(const WifiDirectSocket&) = default;
WifiDirectSocket& operator=(WifiDirectSocket&&) = default;
// Connect to another WifiDirectSocket, to form a functional low-level
// channel. from this point on, and until Close is called, connection exists.
void Connect(WifiDirectSocket& other) ABSL_LOCKS_EXCLUDED(mutex_);
// Returns the InputStream of the WifiDirectSocket.
// On error, returned stream will report Exception::kIo on any operation.
//
// The returned object is not owned by the caller, and can be invalidated once
// the WifiDirectSocket object is destroyed.
InputStream& GetInputStream() override ABSL_LOCKS_EXCLUDED(mutex_);
InputStream& GetInputStream() override {
return SocketBase::GetInputStream();
}
// Returns the OutputStream of the WifiDirectSocket.
// On error, returned stream will report Exception::kIo on any operation.
//
// The returned object is not owned by the caller, and can be invalidated once
// the WifiDirectSocket object is destroyed.
OutputStream& GetOutputStream() override ABSL_LOCKS_EXCLUDED(mutex_);
// Returns address of a remote WifiDirectSocket or nullptr.
WifiDirectSocket* GetRemoteSocket() ABSL_LOCKS_EXCLUDED(mutex_);
// Returns true if connection exists to the (possibly closed) remote socket.
bool IsConnected() const ABSL_LOCKS_EXCLUDED(mutex_);
OutputStream& GetOutputStream() override {
return SocketBase::GetOutputStream();
}
// Returns Exception::kIo on error, Exception::kSuccess otherwise.
Exception Close() override ABSL_LOCKS_EXCLUDED(mutex_);
private:
void DoClose() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
// Returns true if connection exists to the (possibly closed) remote socket.
bool IsConnectedLocked() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
// Returns InputStream of our side of a connection.
// This is what the remote side is supposed to read from.
// This is a helper for GetInputStream() method.
InputStream& GetLocalInputStream() ABSL_LOCKS_EXCLUDED(mutex_);
// Returns OutputStream of our side of a connection.
// This is what the local size is supposed to write to.
// This is a helper for GetOutputStream() method.
OutputStream& GetLocalOutputStream() ABSL_LOCKS_EXCLUDED(mutex_);
// Output pipe is initialized by constructor, it remains always valid, until
// it is closed. it represents output part of a local socket. Input part of a
// local socket comes from the peer socket, after connection.
std::shared_ptr<Pipe> output_{new Pipe};
std::shared_ptr<Pipe> input_;
mutable absl::Mutex mutex_;
WifiDirectSocket* remote_socket_ ABSL_GUARDED_BY(mutex_) = nullptr;
bool closed_ ABSL_GUARDED_BY(mutex_) = false;
Exception Close() override { return SocketBase::Close(); }
};
// WifiDirectServerSocket provides the support to server socket, this server
@@ -16,86 +16,26 @@
#include <iostream>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "absl/log/check.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/mutex.h"
#include "internal/platform/cancellation_flag.h"
#include "internal/platform/cancellation_flag_listener.h"
#include "internal/platform/exception.h"
#include "internal/platform/implementation/wifi_hotspot.h"
#include "internal/platform/logging.h"
#include "internal/platform/medium_environment.h"
#include "internal/platform/prng.h"
#include "internal/platform/wifi_credential.h"
namespace nearby {
namespace g3 {
// Code for WifiHotspotSocket
WifiHotspotSocket::~WifiHotspotSocket() {
absl::MutexLock lock(&mutex_);
DoClose();
}
void WifiHotspotSocket::Connect(WifiHotspotSocket& other) {
absl::MutexLock lock(&mutex_);
remote_socket_ = &other;
input_ = other.output_;
}
InputStream& WifiHotspotSocket::GetInputStream() {
auto* remote_socket = GetRemoteSocket();
CHECK(remote_socket != nullptr);
return remote_socket->GetLocalInputStream();
}
OutputStream& WifiHotspotSocket::GetOutputStream() {
return GetLocalOutputStream();
}
WifiHotspotSocket* WifiHotspotSocket::GetRemoteSocket() {
absl::MutexLock lock(&mutex_);
return remote_socket_;
}
bool WifiHotspotSocket::IsConnected() const {
absl::MutexLock lock(&mutex_);
return IsConnectedLocked();
}
bool WifiHotspotSocket::IsClosed() const {
absl::MutexLock lock(&mutex_);
return closed_;
}
Exception WifiHotspotSocket::Close() {
absl::MutexLock lock(&mutex_);
DoClose();
return {Exception::kSuccess};
}
void WifiHotspotSocket::DoClose() {
if (!closed_) {
remote_socket_ = nullptr;
output_->GetOutputStream().Close();
output_->GetInputStream().Close();
input_->GetOutputStream().Close();
input_->GetInputStream().Close();
closed_ = true;
}
}
bool WifiHotspotSocket::IsConnectedLocked() const { return input_ != nullptr; }
InputStream& WifiHotspotSocket::GetLocalInputStream() {
absl::MutexLock lock(&mutex_);
return output_->GetInputStream();
}
OutputStream& WifiHotspotSocket::GetLocalOutputStream() {
absl::MutexLock lock(&mutex_);
return output_->GetOutputStream();
}
// Code for WifiHotspotServerSocket
std::string WifiHotspotServerSocket::GetName(absl::string_view ip_address,
int port) {
@@ -23,7 +23,7 @@
#include "absl/synchronization/mutex.h"
#include "internal/platform/byte_array.h"
#include "internal/platform/implementation/g3/multi_thread_executor.h"
#include "internal/platform/implementation/g3/pipe.h"
#include "internal/platform/implementation/g3/socket_base.h"
#include "internal/platform/implementation/wifi_hotspot.h"
#include "internal/platform/input_stream.h"
#include "internal/platform/output_stream.h"
@@ -33,69 +33,28 @@ namespace g3 {
class WifiHotspotMedium;
class WifiHotspotSocket : public api::WifiHotspotSocket {
class WifiHotspotSocket : public api::WifiHotspotSocket, public SocketBase {
public:
WifiHotspotSocket() = default;
~WifiHotspotSocket() override;
WifiHotspotSocket(const WifiHotspotSocket&) = default;
WifiHotspotSocket(WifiHotspotSocket&&) = default;
WifiHotspotSocket& operator=(const WifiHotspotSocket&) = default;
WifiHotspotSocket& operator=(WifiHotspotSocket&&) = default;
// Connect to another WifiHotspotSocket, to form a functional low-level
// channel. from this point on, and until Close is called, connection exists.
void Connect(WifiHotspotSocket& other) ABSL_LOCKS_EXCLUDED(mutex_);
// Returns the InputStream of the WifiHotspotSocket.
// On error, returned stream will report Exception::kIo on any operation.
//
// The returned object is not owned by the caller, and can be invalidated once
// the WifiHotspotSocket object is destroyed.
InputStream& GetInputStream() override ABSL_LOCKS_EXCLUDED(mutex_);
InputStream& GetInputStream() override {
return SocketBase::GetInputStream();
}
// Returns the OutputStream of the WifiHotspotSocket.
// On error, returned stream will report Exception::kIo on any operation.
//
// The returned object is not owned by the caller, and can be invalidated once
// the WifiHotspotSocket object is destroyed.
OutputStream& GetOutputStream() override ABSL_LOCKS_EXCLUDED(mutex_);
// Returns address of a remote WifiHotspotSocket or nullptr.
WifiHotspotSocket* GetRemoteSocket() ABSL_LOCKS_EXCLUDED(mutex_);
// Returns true if connection exists to the (possibly closed) remote socket.
bool IsConnected() const ABSL_LOCKS_EXCLUDED(mutex_);
// Returns true if socket is closed.
bool IsClosed() const ABSL_LOCKS_EXCLUDED(mutex_);
OutputStream& GetOutputStream() override {
return SocketBase::GetOutputStream();
}
// Returns Exception::kIo on error, Exception::kSuccess otherwise.
Exception Close() override ABSL_LOCKS_EXCLUDED(mutex_);
private:
void DoClose() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
// Returns true if connection exists to the (possibly closed) remote socket.
bool IsConnectedLocked() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
// Returns InputStream of our side of a connection.
// This is what the remote side is supposed to read from.
// This is a helper for GetInputStream() method.
InputStream& GetLocalInputStream() ABSL_LOCKS_EXCLUDED(mutex_);
// Returns OutputStream of our side of a connection.
// This is what the local size is supposed to write to.
// This is a helper for GetOutputStream() method.
OutputStream& GetLocalOutputStream() ABSL_LOCKS_EXCLUDED(mutex_);
// Output pipe is initialized by constructor, it remains always valid, until
// it is closed. it represents output part of a local socket. Input part of a
// local socket comes from the peer socket, after connection.
std::shared_ptr<Pipe> output_{new Pipe};
std::shared_ptr<Pipe> input_;
mutable absl::Mutex mutex_;
WifiHotspotSocket* remote_socket_ ABSL_GUARDED_BY(mutex_) = nullptr;
bool closed_ ABSL_GUARDED_BY(mutex_) = false;
Exception Close() override { return SocketBase::Close(); }
};
// WifiHotspotServerSocket provides the support to server socket, this server
@@ -19,10 +19,13 @@
#include <string>
#include <utility>
#include "absl/strings/escaping.h"
#include "absl/log/check.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/synchronization/mutex.h"
#include "internal/platform/cancellation_flag.h"
#include "internal/platform/cancellation_flag_listener.h"
#include "internal/platform/exception.h"
#include "internal/platform/implementation/wifi_lan.h"
#include "internal/platform/logging.h"
#include "internal/platform/medium_environment.h"
@@ -31,71 +34,6 @@
namespace nearby {
namespace g3 {
WifiLanSocket::~WifiLanSocket() {
absl::MutexLock lock(&mutex_);
DoClose();
}
void WifiLanSocket::Connect(WifiLanSocket& other) {
absl::MutexLock lock(&mutex_);
remote_socket_ = &other;
input_ = other.output_;
}
InputStream& WifiLanSocket::GetInputStream() {
auto* remote_socket = GetRemoteSocket();
CHECK(remote_socket != nullptr);
return remote_socket->GetLocalInputStream();
}
OutputStream& WifiLanSocket::GetOutputStream() {
return GetLocalOutputStream();
}
WifiLanSocket* WifiLanSocket::GetRemoteSocket() {
absl::MutexLock lock(&mutex_);
return remote_socket_;
}
bool WifiLanSocket::IsConnected() const {
absl::MutexLock lock(&mutex_);
return IsConnectedLocked();
}
bool WifiLanSocket::IsClosed() const {
absl::MutexLock lock(&mutex_);
return closed_;
}
Exception WifiLanSocket::Close() {
absl::MutexLock lock(&mutex_);
DoClose();
return {Exception::kSuccess};
}
void WifiLanSocket::DoClose() {
if (!closed_) {
remote_socket_ = nullptr;
output_->GetOutputStream().Close();
output_->GetInputStream().Close();
input_->GetOutputStream().Close();
input_->GetInputStream().Close();
closed_ = true;
}
}
bool WifiLanSocket::IsConnectedLocked() const { return input_ != nullptr; }
InputStream& WifiLanSocket::GetLocalInputStream() {
absl::MutexLock lock(&mutex_);
return output_->GetInputStream();
}
OutputStream& WifiLanSocket::GetLocalOutputStream() {
absl::MutexLock lock(&mutex_);
return output_->GetOutputStream();
}
std::string WifiLanServerSocket::GetName(const std::string& ip_address,
int port) {
std::string dot_delimited_string;
+12 -44
View File
@@ -24,7 +24,7 @@
#include "absl/synchronization/mutex.h"
#include "internal/platform/byte_array.h"
#include "internal/platform/implementation/g3/multi_thread_executor.h"
#include "internal/platform/implementation/g3/pipe.h"
#include "internal/platform/implementation/g3/socket_base.h"
#include "internal/platform/implementation/wifi_lan.h"
#include "internal/platform/input_stream.h"
#include "internal/platform/nsd_service_info.h"
@@ -35,58 +35,26 @@ namespace g3 {
class WifiLanMedium;
class WifiLanSocket : public api::WifiLanSocket {
class WifiLanSocket : public api::WifiLanSocket, public SocketBase {
public:
WifiLanSocket() = default;
~WifiLanSocket() override;
// Connect to another WifiLanSocket, to form a functional low-level channel.
// from this point on, and until Close is called, connection exists.
void Connect(WifiLanSocket& other) ABSL_LOCKS_EXCLUDED(mutex_);
// Returns the InputStream of this connected WifiLanSocket.
InputStream& GetInputStream() override ABSL_LOCKS_EXCLUDED(mutex_);
InputStream& GetInputStream() override {
return SocketBase::GetInputStream();
}
// Returns the OutputStream of this connected WifiLanSocket.
// This stream is for local side to write.
OutputStream& GetOutputStream() override ABSL_LOCKS_EXCLUDED(mutex_);
OutputStream& GetOutputStream() override {
return SocketBase::GetOutputStream();
}
// Returns address of a remote WifiLanSocket or nullptr.
WifiLanSocket* GetRemoteSocket() ABSL_LOCKS_EXCLUDED(mutex_);
// Returns true if connection exists to the (possibly closed) remote socket.
bool IsConnected() const ABSL_LOCKS_EXCLUDED(mutex_);
// Returns true if socket is closed.
bool IsClosed() const ABSL_LOCKS_EXCLUDED(mutex_);
WifiLanSocket* GetRemoteSocket() {
return static_cast<WifiLanSocket*>(SocketBase::GetRemoteSocket());
}
// Returns Exception::kIo on error, Exception::kSuccess otherwise.
Exception Close() override ABSL_LOCKS_EXCLUDED(mutex_);
private:
void DoClose() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
// Returns true if connection exists to the (possibly closed) remote socket.
bool IsConnectedLocked() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
// Returns InputStream of our side of a connection.
// This is what the remote side is supposed to read from.
// This is a helper for GetInputStream() method.
InputStream& GetLocalInputStream() ABSL_LOCKS_EXCLUDED(mutex_);
// Returns OutputStream of our side of a connection.
// This is what the local size is supposed to write to.
// This is a helper for GetOutputStream() method.
OutputStream& GetLocalOutputStream() ABSL_LOCKS_EXCLUDED(mutex_);
// Output pipe is initialized by constructor, it remains always valid, until
// it is closed. it represents output part of a local socket. Input part of a
// local socket comes from the peer socket, after connection.
std::shared_ptr<Pipe> output_{new Pipe};
std::shared_ptr<Pipe> input_;
mutable absl::Mutex mutex_;
WifiLanSocket* remote_socket_ ABSL_GUARDED_BY(mutex_) = nullptr;
bool closed_ ABSL_GUARDED_BY(mutex_) = false;
Exception Close() override { return SocketBase::Close(); }
};
class WifiLanServerSocket : public api::WifiLanServerSocket {
@@ -11,6 +11,7 @@
# 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.
licenses(["notice"])
cc_library(
@@ -49,7 +50,6 @@ cc_library(
"//base:stringprintf",
"//internal/base:bluetooth_address",
"//internal/platform:base",
"//internal/platform:logging",
"//internal/platform:types",
"//internal/platform:uuid",
"//internal/platform/implementation:types",
@@ -95,6 +95,7 @@ cc_library(
"mutex.h",
"scheduled_executor.h",
"server_sync.h",
"session_manager.h",
"submittable_executor.h",
"thread_pool.h",
"webrtc.h",
@@ -166,6 +167,7 @@ cc_library(
"preferences_repository.cc",
"preferences_repository.h",
"scheduled_executor.cc",
"session_manager.cc",
"submittable_executor.cc",
"system_clock.cc",
"thread_pool.cc",
@@ -202,7 +204,6 @@ cc_library(
"//internal/platform:base",
"//internal/platform:cancellation_flag",
"//internal/platform:comm",
"//internal/platform:logging",
"//internal/platform:types",
"//internal/platform:uuid",
"//internal/platform/flags:platform_flags",
@@ -283,7 +284,7 @@ cc_test(
":types",
":windows",
"//internal/platform:base",
"//internal/platform:logging",
"//internal/platform:types",
"//internal/platform/implementation:comm",
"//internal/platform/implementation:platform",
"//internal/platform/implementation:types",
@@ -47,6 +47,7 @@
#include "winrt/Windows.Devices.Bluetooth.Advertisement.h"
#include "winrt/Windows.Devices.Bluetooth.h"
#include "winrt/Windows.Foundation.Collections.h"
#include "winrt/Windows.Storage.Streams.h" // NOLINT(misc-include-cleaner)
namespace nearby {
namespace windows {
@@ -68,6 +69,8 @@ using ::winrt::Windows::Devices::Bluetooth::Advertisement::
BluetoothLEAdvertisementDataSection;
using ::winrt::Windows::Devices::Bluetooth::Advertisement::
BluetoothLEAdvertisementDataTypes;
using ::winrt::Windows::Devices::Bluetooth::Advertisement::
BluetoothLEAdvertisementFilter; // NOLINT(misc-include-cleaner)
using ::winrt::Windows::Devices::Bluetooth::Advertisement::
BluetoothLEAdvertisementPublisher;
using ::winrt::Windows::Devices::Bluetooth::Advertisement::
@@ -230,9 +233,19 @@ bool BleV2Medium::StartScanning(const Uuid& service_uuid,
// Active mode indicates that scan request packets will be sent to query for
// Scan Response
watcher_.ScanningMode(BluetoothLEScanningMode::Active);
::winrt::Windows::Devices::Bluetooth::BluetoothSignalStrengthFilter filter;
filter.SamplingInterval(TimeSpan(std::chrono::seconds(2)));
watcher_.SignalStrengthFilter(filter);
BluetoothLEAdvertisementDataSection data_section;
data_section.DataType(0x16);
DataWriter data_writer; // NOLINT(misc-include-cleaner)
std::array<char, 16> service_id_data = service_uuid_.data();
data_writer.WriteByte(service_id_data[3] & 0xff);
data_writer.WriteByte(service_id_data[2] & 0xff);
data_section.Data(data_writer.DetachBuffer());
BluetoothLEAdvertisement advertisement;
advertisement.DataSections().Append(data_section);
BluetoothLEAdvertisementFilter advertisement_filter;
advertisement_filter.Advertisement(advertisement);
watcher_.AdvertisementFilter(advertisement_filter);
watcher_.Start();
is_watcher_started_ = true;
@@ -311,7 +311,10 @@ std::unique_ptr<api::BluetoothSocket> BluetoothClassicMedium::ConnectToService(
RfcommDeviceService requested_service(
GetRequestedService(current_device, service));
if (!CheckSdp(requested_service)) {
if (!FeatureFlags::GetInstance()
.GetFlags()
.skip_service_discovery_before_connecting_to_rfcomm &&
!CheckSdp(requested_service)) {
NEARBY_LOGS(ERROR) << __func__ << ": Invalid SDP.";
return nullptr;
}
@@ -23,11 +23,14 @@
#include <functional>
#include <optional>
#include <string>
#include <utility>
#include "absl/container/flat_hash_map.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/mutex.h"
#include "internal/base/bluetooth_address.h"
#include "internal/platform/implementation/device_info.h"
#include "internal/platform/implementation/windows/session_manager.h"
#include "internal/platform/logging.h"
#include "winrt/Windows.Foundation.Collections.h"
#include "winrt/Windows.Foundation.h"
@@ -49,89 +52,10 @@ using IVectorView = winrt::Windows::Foundation::Collections::IVectorView<T>;
template <typename T>
using IAsyncOperation = winrt::Windows::Foundation::IAsyncOperation<T>;
constexpr char window_class_name[] = "NearbySharingDLL_MessageWindowClass";
constexpr char window_name[] = "NearbySharingDLL_MessageWindow";
constexpr char logs_relative_path[] = "Google\\Nearby\\Sharing\\Logs";
constexpr char crash_dumps_relative_path[] =
"Google\\Nearby\\Sharing\\CrashDumps";
namespace {
// This WindowProc method must be static for the successful initialization of
// WNDCLASS
// window_class.lpfnWndProc = (WNDPROC) &DeviceInfo::WindowProc;
// where a WNDPROC typed function pointer is expected
// typedef LRESULT (CALLBACK* WNDPROC)(HWND,UINT,WPARAM,LPARAM)
// the calling convention used here CALLBACK is a macro defined as
// #define CALLBACK __stdcall
//
// If WindProc is not static and defined as a member function, it uses the
// __thiscall calling convention instead
// https://docs.microsoft.com/en-us/cpp/cpp/thiscall?view=msvc-170
// https://isocpp.org/wiki/faq/pointers-to-members
// https://en.cppreference.com/w/cpp/language/pointer
//
// This is problematic because the function pointer now looks like this
// typedef LRESULT (CALLBACK* DeviceInfo_WNDPROC)(DeviceInfo*
// this,HWND,UINT,WPARAM,LPARAM)
// which causes casting errors
LRESULT CALLBACK WindowProc(HWND window_handle, UINT message, WPARAM wparam,
LPARAM lparam) {
DeviceInfo* self = reinterpret_cast<DeviceInfo*>(
GetWindowLongPtr(window_handle, GWLP_USERDATA));
CREATESTRUCT* create_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
LONG_PTR result = 0L;
switch (message) {
case WM_CREATE:
self = reinterpret_cast<DeviceInfo*>(create_struct->lpCreateParams);
self->message_window_handle_ = window_handle;
// Store pointer to the self to the window's user data.
SetLastError(0);
result = SetWindowLongPtr(window_handle, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(self));
if (result == 0 && GetLastError() != 0) {
NEARBY_LOGS(ERROR)
<< __func__
<< ": Error connecting message window to Nearby Sharing DLL.";
}
break;
case WM_WTSSESSION_CHANGE:
if (self) {
switch (wparam) {
case WTS_SESSION_LOCK:
for (auto& listener : self->screen_locked_listeners_) {
listener.second(api::DeviceInfo::ScreenStatus::
kLocked); // Trigger registered callbacks
}
break;
case WTS_SESSION_UNLOCK:
for (auto& listener : self->screen_locked_listeners_) {
listener.second(api::DeviceInfo::ScreenStatus::
kUnlocked); // Trigger registered callbacks
}
break;
}
}
break;
case WM_DESTROY:
SetLastError(0);
result = SetWindowLongPtr(window_handle, GWLP_USERDATA, NULL);
if (result == 0 && GetLastError() != 0) {
NEARBY_LOGS(ERROR)
<< __func__
<< ": Error disconnecting message window to Nearby Sharing DLL.";
}
break;
}
return DefWindowProc(window_handle, message, wparam, lparam);
}
} // namespace
DeviceInfo::~DeviceInfo() {
UnregisterClass(MAKEINTATOM(registered_class_), instance_);
}
std::optional<std::u16string> DeviceInfo::GetOsDeviceName() const {
DWORD size = 0;
@@ -404,78 +328,39 @@ std::optional<std::filesystem::path> DeviceInfo::GetCrashDumpPath() const {
}
bool DeviceInfo::IsScreenLocked() const {
DWORD session_id = WTSGetActiveConsoleSessionId();
WTS_INFO_CLASS wts_info_class = WTSSessionInfoEx;
LPTSTR session_info_buffer = nullptr;
DWORD session_info_buffer_size_bytes = 0;
WTSINFOEXW* wts_info = nullptr;
LONG session_state = WTS_SESSIONSTATE_UNKNOWN;
if (WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, session_id,
wts_info_class, &session_info_buffer,
&session_info_buffer_size_bytes)) {
if (session_info_buffer_size_bytes > 0) {
wts_info = (WTSINFOEXW*)session_info_buffer;
if (wts_info->Level == 1) {
session_state = wts_info->Data.WTSInfoExLevel1.SessionFlags;
}
}
WTSFreeMemory(session_info_buffer);
session_info_buffer = nullptr;
}
return (session_state == WTS_SESSIONSTATE_LOCK);
absl::MutexLock lock(&mutex_);
return session_manager_.IsScreenLocked();
}
void DeviceInfo::RegisterScreenLockedListener(
absl::string_view listener_name,
std::function<void(api::DeviceInfo::ScreenStatus)> callback) {
if (message_window_handle_ == nullptr) {
instance_ = (HINSTANCE)GetModuleHandle(NULL);
WNDCLASS window_class;
window_class.style = 0;
window_class.lpfnWndProc = (WNDPROC)&WindowProc;
window_class.cbClsExtra = 0;
window_class.cbWndExtra = 0;
window_class.hInstance = instance_;
window_class.hIcon = nullptr;
window_class.hCursor = nullptr;
window_class.hbrBackground = nullptr;
window_class.lpszMenuName = nullptr;
window_class.lpszClassName = window_class_name;
registered_class_ = RegisterClass(&window_class);
message_window_handle_ = CreateWindow(
MAKEINTATOM(registered_class_), // class atom from RegisterClass
window_name, // window name
0, // window style
0, // initial x position of window
0, // initial y position of window
0, // width
0, // height
HWND_MESSAGE, // handle to the parent of window
// (message-only window in this case)
nullptr, // handle to a menu
instance_, // handle to the instance of the module
// associated to the window
this); // pointer to be passed to the window for additional data
if (!message_window_handle_) {
NEARBY_LOGS(ERROR)
<< __func__
<< ": Failed to create message window for Nearby Sharing DLL.";
}
}
screen_locked_listeners_.emplace(listener_name, callback);
absl::MutexLock lock(&mutex_);
session_manager_.RegisterSessionListener(
listener_name,
[callback = std::move(callback)](SessionManager::SessionState state) {
if (state == SessionManager::SessionState::kLock) {
callback(api::DeviceInfo::ScreenStatus::kLocked);
} else if (state == SessionManager::SessionState::kUnlock) {
callback(api::DeviceInfo::ScreenStatus::kUnlocked);
}
});
}
void DeviceInfo::UnregisterScreenLockedListener(
absl::string_view listener_name) {
screen_locked_listeners_.erase(listener_name);
absl::MutexLock lock(&mutex_);
session_manager_.UnregisterSessionListener(listener_name);
}
bool DeviceInfo::PreventSleep() {
absl::MutexLock lock(&mutex_);
return session_manager_.PreventSleep();
}
bool DeviceInfo::AllowSleep() {
absl::MutexLock lock(&mutex_);
return session_manager_.AllowSleep();
}
} // namespace windows
@@ -1,4 +1,4 @@
// Copyright 2021 Google LLC
// Copyright 2021-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.
@@ -15,25 +15,21 @@
#ifndef PLATFORM_IMPL_WINDOWS_DEVICE_INFO_H_
#define PLATFORM_IMPL_WINDOWS_DEVICE_INFO_H_
#include <guiddef.h>
#include <windows.h>
#include <array>
#include <functional>
#include <optional>
#include <string>
#include "absl/container/flat_hash_map.h"
#include "absl/base/thread_annotations.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/mutex.h"
#include "internal/platform/implementation/device_info.h"
#include "winrt/Windows.Foundation.h"
#include "internal/platform/implementation/windows/session_manager.h"
namespace nearby {
namespace windows {
class DeviceInfo : public api::DeviceInfo {
public:
~DeviceInfo() override;
~DeviceInfo() override = default;
std::optional<std::u16string> GetOsDeviceName() const override;
api::DeviceInfo::DeviceType GetDeviceType() const override;
@@ -55,13 +51,13 @@ class DeviceInfo : public api::DeviceInfo {
absl::string_view listener_name,
std::function<void(api::DeviceInfo::ScreenStatus)> callback) override;
void UnregisterScreenLockedListener(absl::string_view listener_name) override;
absl::flat_hash_map<std::string,
std::function<void(api::DeviceInfo::ScreenStatus)>>
screen_locked_listeners_;
HINSTANCE instance_ = nullptr;
ATOM registered_class_ = NULL;
HWND message_window_handle_ = nullptr;
bool PreventSleep() override;
bool AllowSleep() override;
private:
mutable absl::Mutex mutex_;
SessionManager session_manager_ ABSL_GUARDED_BY(mutex_);
};
} // namespace windows
@@ -77,59 +77,12 @@ TEST(DeviceInfo, DISABLED_IsScreenLocked) {
EXPECT_FALSE(DeviceInfo().IsScreenLocked());
}
TEST(DeviceInfo, DISABLED_RegisterScreenLockedListener) {
std::function<void(api::DeviceInfo::ScreenStatus)> listener_1 =
[](api::DeviceInfo::ScreenStatus) {};
std::function<void(api::DeviceInfo::ScreenStatus)> listener_2 =
[](api::DeviceInfo::ScreenStatus) {};
DeviceInfo device_info;
EXPECT_EQ(device_info.screen_locked_listeners_.size(), 0);
device_info.RegisterScreenLockedListener("listener_1", listener_1);
EXPECT_EQ(device_info.screen_locked_listeners_.size(), 1);
device_info.RegisterScreenLockedListener("listener_2", listener_2);
EXPECT_EQ(device_info.screen_locked_listeners_.size(), 2);
TEST(DeviceInfo, DISABLED_PreventSleep) {
EXPECT_TRUE(DeviceInfo().PreventSleep());
}
TEST(DeviceInfo, DISABLED_UnregisterScreenLockedListener) {
std::function<void(api::DeviceInfo::ScreenStatus)> listener_1 =
[](api::DeviceInfo::ScreenStatus) {};
std::function<void(api::DeviceInfo::ScreenStatus)> listener_2 =
[](api::DeviceInfo::ScreenStatus) {};
DeviceInfo device_info;
EXPECT_EQ(device_info.screen_locked_listeners_.size(), 0);
device_info.RegisterScreenLockedListener("listener_1", listener_1);
device_info.RegisterScreenLockedListener("listener_2", listener_2);
EXPECT_EQ(device_info.screen_locked_listeners_.size(), 2);
device_info.UnregisterScreenLockedListener("listener_1");
EXPECT_EQ(device_info.screen_locked_listeners_.size(), 1);
device_info.UnregisterScreenLockedListener("listener_2");
EXPECT_EQ(device_info.screen_locked_listeners_.size(), 0);
}
TEST(DeviceInfo, DISABLED_UpdateScreenLockedListener) {
absl::Notification notification;
api::DeviceInfo::ScreenStatus screen_locked_tracker =
api::DeviceInfo::ScreenStatus::kUndetermined;
std::function<void(api::DeviceInfo::ScreenStatus)> listener =
[&screen_locked_tracker,
&notification](api::DeviceInfo::ScreenStatus status) {
screen_locked_tracker = api::DeviceInfo::ScreenStatus::kLocked;
notification.Notify();
};
DeviceInfo device_info;
device_info.RegisterScreenLockedListener("listener", listener);
EXPECT_TRUE(notification.WaitForNotificationWithTimeout(absl::Seconds(5)));
EXPECT_EQ(screen_locked_tracker, api::DeviceInfo::ScreenStatus::kLocked);
TEST(DeviceInfo, DISABLED_AllowSleep) {
EXPECT_TRUE(DeviceInfo().AllowSleep());
}
} // namespace
@@ -0,0 +1,290 @@
// 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.
#include "internal/platform/implementation/windows/session_manager.h"
#include <Windows.h>
#include <wtsapi32.h>
#include <string>
#include <utility>
#include "absl/base/attributes.h"
#include "absl/base/const_init.h"
#include "absl/container/flat_hash_map.h"
#include "absl/functional/any_invocable.h"
#include "absl/synchronization/mutex.h"
#include "absl/synchronization/notification.h"
#include "internal/platform/implementation/windows/submittable_executor.h"
#include "internal/platform/logging.h"
namespace nearby {
namespace windows {
namespace {
constexpr char kMessageWindowClass[] = "Nearby Message Window Class";
constexpr char kMessageWindowTitle[] = "Nearby Message Dummy Window";
LRESULT CALLBACK NearbyWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam,
LPARAM lParam) {
switch (uMsg) {
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_WTSSESSION_CHANGE:
if (wParam == WTS_SESSION_LOCK) {
SessionManager::NotifySessionState(SessionManager::SessionState::kLock);
} else if (wParam == WTS_SESSION_UNLOCK) {
SessionManager::NotifySessionState(
SessionManager::SessionState::kUnlock);
}
break;
}
// NOLINTNEXTLINE(misc-include-cleaner)
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
HWND CreateNearbyWindow() {
HINSTANCE instance = (HINSTANCE)GetModuleHandle(nullptr);
WNDCLASS window_class = {};
window_class.lpfnWndProc = NearbyWindowProc;
window_class.hInstance = instance;
window_class.lpszClassName = kMessageWindowClass;
RegisterClass(&window_class);
HWND hwnd =
CreateWindowA(kMessageWindowClass, kMessageWindowTitle, /*dwStyle=*/0,
/*X=*/0, /*Y=*/0, /*nWidth=*/0, /*nHeight=*/0, HWND_MESSAGE,
/*hMenu=*/nullptr, instance,
/*lpParam=*/nullptr);
return hwnd;
}
} // namespace
// Initialize class static variables.
ABSL_CONST_INIT absl::Mutex SessionManager::session_mutex_{absl::kConstInit};
HWND SessionManager::session_hwnd_ = nullptr;
SubmittableExecutor* SessionManager::session_thread_ = nullptr;
absl::flat_hash_map<std::string,
absl::AnyInvocable<void(SessionManager::SessionState)>>*
SessionManager::session_callbacks_ = nullptr;
SessionManager::~SessionManager() { StopSession(); }
bool SessionManager::RegisterSessionListener(
absl::string_view listener_name,
absl::AnyInvocable<void(SessionState)> callback) {
absl::MutexLock lock(&session_mutex_);
NEARBY_LOGS(INFO) << __func__ << ": Registering listener: " << listener_name;
// Create session thread if no running thread.
if (session_thread_ == nullptr) {
absl::Notification notification;
session_thread_ = new SubmittableExecutor();
session_callbacks_ = new absl::flat_hash_map<
std::string, absl::AnyInvocable<void(SessionManager::SessionState)>>();
session_thread_->Execute(
[this, &notification]() { StartSession(notification); });
notification.WaitForNotification();
if (session_thread_ == nullptr) {
return false;
}
}
if (session_callbacks_->contains(listener_name) ||
listeners_.contains(listener_name)) {
return false;
}
session_callbacks_->emplace(listener_name, std::move(callback));
listeners_.emplace(listener_name);
NEARBY_LOGS(INFO) << __func__ << ": Session listener: " << listener_name
<< " is registered.";
return true;
}
bool SessionManager::UnregisterSessionListener(
absl::string_view listener_name) {
absl::MutexLock lock(&session_mutex_);
NEARBY_LOGS(INFO) << __func__
<< ": Unregistering listener: " << listener_name;
if (session_thread_ == nullptr) {
NEARBY_LOGS(ERROR) << __func__ << ": No running listener.";
return false;
}
if (!session_callbacks_->contains(listener_name) ||
!listeners_.contains(listener_name)) {
NEARBY_LOGS(ERROR) << __func__
<< ": No listener with name:" << listener_name;
return false;
}
session_callbacks_->erase(listener_name);
listeners_.erase(listener_name);
if (!session_callbacks_->empty()) {
NEARBY_LOGS(INFO) << __func__ << ": Session listener: " << listener_name
<< " is unregistered.";
return true;
}
CleanUp();
NEARBY_LOGS(INFO) << __func__ << ": Session listener: " << listener_name
<< " is unregistered.";
return true;
}
bool SessionManager::IsScreenLocked() const {
DWORD session_id = WTSGetActiveConsoleSessionId();
WTS_INFO_CLASS wts_info_class = WTSSessionInfoEx;
LPTSTR session_info_buffer = nullptr;
DWORD session_info_buffer_size_bytes = 0;
WTSINFOEXW* wts_info = nullptr;
LONG session_state = WTS_SESSIONSTATE_UNKNOWN;
if (WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, session_id,
wts_info_class, &session_info_buffer,
&session_info_buffer_size_bytes)) {
if (session_info_buffer_size_bytes > 0) {
wts_info = (WTSINFOEXW*)session_info_buffer;
if (wts_info->Level == 1) {
session_state = wts_info->Data.WTSInfoExLevel1.SessionFlags;
}
}
WTSFreeMemory(session_info_buffer);
session_info_buffer = nullptr;
}
return (session_state == WTS_SESSIONSTATE_LOCK);
}
bool SessionManager::PreventSleep() const {
EXECUTION_STATE execution_state =
SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED);
if (execution_state == 0) {
NEARBY_LOGS(ERROR) << __func__
<< ": Failed to set execution state of the thread.";
return false;
}
return true;
}
bool SessionManager::AllowSleep() const {
EXECUTION_STATE execution_state = SetThreadExecutionState(ES_CONTINUOUS);
if (execution_state == 0) {
NEARBY_LOGS(ERROR) << __func__
<< ": Failed to set execution state of the thread.";
return false;
}
return true;
}
void SessionManager::NotifySessionState(SessionState state) {
NEARBY_LOGS(INFO) << __func__
<< ": Notifying session state: " << static_cast<int>(state);
if (state == SessionManager::SessionState::kLock) {
absl::MutexLock lock(&session_mutex_);
for (auto& it : *SessionManager::session_callbacks_) {
it.second(SessionManager::SessionState::kLock);
}
} else if (state == SessionManager::SessionState::kUnlock) {
absl::MutexLock lock(&session_mutex_);
for (auto& it : *SessionManager::session_callbacks_) {
it.second(SessionManager::SessionState::kUnlock);
}
}
}
void SessionManager::StartSession(absl::Notification& notification) {
session_hwnd_ = CreateNearbyWindow();
if (session_hwnd_ == nullptr) {
NEARBY_LOGS(ERROR) << __func__ << ": Failed to create session Window.";
return;
}
if (!WTSRegisterSessionNotification(session_hwnd_, NOTIFY_FOR_THIS_SESSION)) {
NEARBY_LOGS(ERROR) << __func__
<< ":Failed to register session notification.";
return;
}
notification.Notify();
NEARBY_LOGS(INFO) << __func__ << ": Session thread started.";
// Main message loop
MSG msg = {};
// NOLINTNEXTLINE(misc-include-cleaner)
while (GetMessage(&msg, session_hwnd_, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
if (!WTSUnRegisterSessionNotification(session_hwnd_)) {
NEARBY_LOGS(ERROR) << __func__
<< ": Failed to register session notification.";
return;
}
if (!UnregisterClassA(/*lpClassName=*/kMessageWindowClass,
/*hInstance=*/(HINSTANCE)GetModuleHandle(nullptr))) {
NEARBY_LOGS(ERROR) << __func__ << ": Failed to unregister window class.";
}
NEARBY_LOGS(INFO) << __func__ << ": Completed Message loop.";
}
void SessionManager::StopSession() {
if (session_thread_ == nullptr) {
return;
}
absl::MutexLock lock(&session_mutex_);
if (session_thread_ == nullptr) {
return;
}
for (const auto& it : listeners_) {
session_callbacks_->erase(it);
}
listeners_.clear();
if (!session_callbacks_->empty()) {
return;
}
CleanUp();
}
void SessionManager::CleanUp() {
if (session_hwnd_ != nullptr) {
// Send message to destroy message window.
// NOLINTNEXTLINE(misc-include-cleaner)
PostMessageA(session_hwnd_, WM_DESTROY, 0, 0);
}
session_thread_->Shutdown();
delete session_thread_;
delete session_callbacks_;
session_thread_ = nullptr;
session_callbacks_ = nullptr;
session_hwnd_ = nullptr;
}
} // namespace windows
} // namespace nearby
@@ -0,0 +1,76 @@
// 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.
#ifndef THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_WINDOWS_SESSION_MANAGER_H_
#define THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_WINDOWS_SESSION_MANAGER_H_
#include <string>
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
#include "absl/functional/any_invocable.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/mutex.h"
#include "absl/synchronization/notification.h"
#include "internal/platform/implementation/windows/submittable_executor.h"
namespace nearby {
namespace windows {
// SessionManager provides methods to access/control platform session.
class SessionManager {
public:
enum class SessionState { kLock, kUnlock };
~SessionManager();
// Setups session listener.
// listener_name - Listener name. it should be unique in the SDK level.
// callback - It will be called when session state changed, such as
// lock/unlock screen.
bool RegisterSessionListener(absl::string_view listener_name,
absl::AnyInvocable<void(SessionState)> callback);
// Removes session listener by its name.
bool UnregisterSessionListener(absl::string_view listener_name);
bool IsScreenLocked() const;
// Prevents the Windows device from sleeping state.
bool PreventSleep() const;
// Allows the Windows device to sleep state.
bool AllowSleep() const;
static void NotifySessionState(SessionState state);
private:
void StartSession(absl::Notification& notification);
void StopSession();
void CleanUp();
absl::flat_hash_set<std::string> listeners_;
// Static class members
static absl::Mutex session_mutex_;
static HWND session_hwnd_;
static SubmittableExecutor* session_thread_;
static absl::flat_hash_map<
std::string, absl::AnyInvocable<void(SessionManager::SessionState)>>*
session_callbacks_;
};
} // namespace windows
} // namespace nearby
#endif // THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_WINDOWS_SESSION_MANAGER_H_
@@ -1,4 +1,4 @@
// Copyright 2021-2023 Google LLC
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,9 +14,10 @@
#include "internal/platform/implementation/windows/timer.h"
#include "absl/functional/any_invocable.h"
#include <memory>
#include "absl/synchronization/mutex.h"
#include "internal/platform/logging.h"
#include "internal/platform/mutex_lock.h"
namespace nearby {
namespace windows {
@@ -25,7 +26,7 @@ Timer::~Timer() { Stop(); }
bool Timer::Create(int delay, int interval,
absl::AnyInvocable<void()> callback) {
MutexLock lock(&mutex_);
absl::MutexLock lock(&mutex_);
if ((delay < 0) || (interval < 0)) {
NEARBY_LOGS(WARNING) << "Delay and interval shouldn\'t be negative value.";
@@ -45,17 +46,10 @@ bool Timer::Create(int delay, int interval,
delay_ = delay;
interval_ = interval;
callback_ = std::move(callback);
timer_callback_ = [&]() {
MutexLock lock(&mutex_);
if (timer_queue_handle_ != nullptr && callback_ != nullptr) {
callback_();
}
};
if (!CreateTimerQueueTimer(&handle_, timer_queue_handle_,
static_cast<WAITORTIMERCALLBACK>(TimerRoutine),
&timer_callback_, delay, interval,
WT_EXECUTEDEFAULT)) {
&callback_, delay, interval, WT_EXECUTEDEFAULT)) {
if (!DeleteTimerQueueEx(timer_queue_handle_, nullptr)) {
NEARBY_LOGS(ERROR) << "Failed to create timer in timer queue.";
}
@@ -67,7 +61,7 @@ bool Timer::Create(int delay, int interval,
}
bool Timer::Stop() {
MutexLock lock(&mutex_);
absl::MutexLock lock(&mutex_);
if (timer_queue_handle_ == nullptr) {
return true;
@@ -92,7 +86,7 @@ bool Timer::Stop() {
}
bool Timer::FireNow() {
MutexLock lock(&mutex_);
absl::MutexLock lock(&mutex_);
if (!timer_queue_handle_ || !callback_) {
return false;
@@ -107,7 +101,8 @@ bool Timer::FireNow() {
<< "Failed to fire the task due to cannot create executor.";
return false;
}
task_executor_->Execute([&]() { timer_callback_(); });
task_executor_->Execute([&]() { callback_(); });
return true;
}
@@ -1,4 +1,4 @@
// Copyright 2021-2023 Google LLC
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -20,10 +20,9 @@
#include <memory>
#include "absl/base/thread_annotations.h"
#include "absl/functional/any_invocable.h"
#include "absl/synchronization/mutex.h"
#include "internal/platform/implementation/timer.h"
#include "internal/platform/implementation/windows/submittable_executor.h"
#include "internal/platform/mutex.h"
namespace nearby {
namespace windows {
@@ -42,11 +41,10 @@ class Timer : public api::Timer {
private:
static void CALLBACK TimerRoutine(PVOID lpParam, BOOLEAN TimerOrWaitFired);
mutable RecursiveMutex mutex_;
mutable absl::Mutex mutex_;
int delay_ ABSL_GUARDED_BY(mutex_);
int interval_ ABSL_GUARDED_BY(mutex_);
absl::AnyInvocable<void()> callback_;
absl::AnyInvocable<void()> timer_callback_ = nullptr;
HANDLE handle_ ABSL_GUARDED_BY(mutex_) = nullptr;
HANDLE timer_queue_handle_ ABSL_GUARDED_BY(mutex_) = nullptr;
std::unique_ptr<SubmittableExecutor> task_executor_ ABSL_GUARDED_BY(mutex_) =
@@ -1,4 +1,4 @@
// Copyright 2021-2023 Google LLC
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,8 +14,12 @@
#include "internal/platform/implementation/timer.h"
#include <chrono> // NOLINT
// NOLINT
#include <memory>
#include <thread> // NOLINT
#include "gtest/gtest.h"
#include "absl/time/time.h"
#include "internal/platform/implementation/platform.h"
namespace nearby {
@@ -42,7 +46,7 @@ TEST(Timer, DISABLED_TestRepeatTimer) {
ASSERT_TRUE(timer != nullptr);
EXPECT_TRUE(timer->Create(300, 300, [&]() { ++count; }));
absl::SleepFor(absl::Seconds(1));
std::this_thread::sleep_for(std::chrono::seconds(1));
EXPECT_TRUE(timer->Stop());
EXPECT_EQ(count, 3);
}
@@ -53,27 +57,8 @@ TEST(Timer, DISABLED_TestFireNow) {
auto timer = nearby::api::ImplementationPlatform::CreateTimer();
EXPECT_TRUE(timer != nullptr);
EXPECT_TRUE(timer->Create(3000, 3000, [&]() {
absl::SleepFor(absl::Milliseconds(1000));
++count;
}));
EXPECT_TRUE(timer->Create(3000, 3000, [&]() { ++count; }));
EXPECT_TRUE(timer->FireNow());
absl::SleepFor(absl::Milliseconds(100));
EXPECT_TRUE(timer->Stop());
EXPECT_EQ(count, 1);
}
TEST(Timer, DISABLED_TestWaitForRunningCallback) {
int count = 0;
auto timer = nearby::api::ImplementationPlatform::CreateTimer();
EXPECT_TRUE(timer != nullptr);
EXPECT_TRUE(timer->Create(1000, 0, [&]() {
absl::SleepFor(absl::Milliseconds(3000));
++count;
}));
absl::SleepFor(absl::Milliseconds(1050));
EXPECT_TRUE(timer->Stop());
EXPECT_EQ(count, 1);
}
@@ -16,8 +16,8 @@
#define PLATFORM_IMPL_WINDOWS_WIFI_HOTSPOT_H_
// Windows headers
#include <winsock2.h>
#include <windows.h>
#include <winsock2.h>
#include <wlanapi.h>
// Standard C/C++ headers
@@ -27,6 +27,7 @@
#include <string>
// Nearby connections headers
#include "absl/strings/string_view.h"
#include "internal/platform/implementation/wifi_hotspot.h"
#include "internal/platform/implementation/windows/scheduled_executor.h"
#include "internal/platform/implementation/windows/submittable_executor.h"
@@ -123,7 +124,7 @@ class WifiHotspotSocket : public api::WifiHotspotSocket {
Exception Close() override;
private:
enum class SocketType {kWinRTSocket = 0, kWin32Socket};
enum class SocketType { kWinRTSocket = 0, kWin32Socket };
// A simple wrapper to handle input stream of socket
class SocketInputStream : public InputStream {
public:
@@ -204,6 +205,10 @@ class WifiHotspotServerSocket : public api::WifiHotspotServerSocket {
bool listen();
private:
static constexpr int kSocketEventsCount = 2;
static constexpr int kSocketEventListen = 0;
static constexpr int kSocketEventClose = 1;
// The listener is accepting incoming connections
fire_and_forget Listener_ConnectionReceived(
StreamSocketListener listener,
@@ -214,6 +219,8 @@ class WifiHotspotServerSocket : public api::WifiHotspotServerSocket {
// Retrieves hotspot IP address from local machine
std::string GetHotspotIpAddress() const;
void SocketErrorNotice(absl::string_view reason);
mutable absl::Mutex mutex_;
absl::CondVar cond_;
SubmittableExecutor submittable_executor_;
@@ -225,6 +232,13 @@ class WifiHotspotServerSocket : public api::WifiHotspotServerSocket {
std::deque<SOCKET> pending_client_sockets_ ABSL_GUARDED_BY(mutex_);
SOCKET listen_socket_ = INVALID_SOCKET;
SOCKET client_socket_ = INVALID_SOCKET;
// closesocket cannot trigger FD_CLOSE on listener socket. In order to avoid
// blocking in WSAWaitForMultipleEvents, we use a socket event to trigger
// WSAWaitForMultipleEvents safely.
// The socket_events_ has 2 events, the first one is to handle normal socket
// event, and the second one is to handle event to close the socket manually.
WSAEVENT socket_events_[kSocketEventsCount];
// Close notifier
absl::AnyInvocable<void()> close_notifier_ = nullptr;
@@ -36,7 +36,11 @@ namespace {
using ::winrt::Windows::Networking::Sockets::SocketQualityOfService;
} // namespace
WifiHotspotServerSocket::WifiHotspotServerSocket(int port) : port_(port) {}
WifiHotspotServerSocket::WifiHotspotServerSocket(int port) : port_(port) {
for (auto &it : socket_events_) {
it = WSA_INVALID_EVENT;
}
}
WifiHotspotServerSocket::~WifiHotspotServerSocket() { Close(); }
@@ -55,7 +59,7 @@ std::string WifiHotspotServerSocket::GetIPAddress() const {
std::string hotspot_ip_address = GetHotspotIpAddress();
NEARBY_LOGS(INFO) << __func__
<< ": Return hotspot IP address: " << hotspot_ip_address;
<< ": Return hotspot IP address: " << hotspot_ip_address;
return hotspot_ip_address;
}
@@ -126,18 +130,26 @@ Exception WifiHotspotServerSocket::Close() {
kEnableHotspotWin32Socket)) {
if (listen_socket_ != INVALID_SOCKET) {
NEARBY_LOGS(INFO) << ": Close listen_socket_: " << listen_socket_;
// Trigger close event manually
WSASetEvent(socket_events_[kSocketEventClose]);
shutdown(listen_socket_, 2);
shutdown(client_socket_, 2);
closesocket(listen_socket_);
closesocket(client_socket_);
listen_socket_ = INVALID_SOCKET;
client_socket_ = INVALID_SOCKET;
for (const auto &pending_socket : pending_client_sockets_) {
if (pending_socket != INVALID_SOCKET) closesocket(pending_socket);
}
submittable_executor_.Shutdown();
listen_socket_ = INVALID_SOCKET;
client_socket_ = INVALID_SOCKET;
for (auto &it : socket_events_) {
WSACloseEvent(it);
it = WSA_INVALID_EVENT;
}
WSACleanup();
pending_client_sockets_ = {};
}
submittable_executor_.Shutdown();
} else {
if (stream_socket_listener_ != nullptr) {
stream_socket_listener_.ConnectionReceived(listener_event_token_);
@@ -254,14 +266,16 @@ bool WifiHotspotServerSocket::SetupServerSocketWinRT() {
return false;
}
// Checks for SOCKET_ERROR, this error can come up when trying to bind, listen,
// Getsockname, WSACreateEvent, WSAEventSelect etc.
void SocketErrorNotice(SOCKET socket_to_close, const char *action) {
// const char *actionAttempted = action;
NEARBY_LOGS(WARNING) << "socket error. " << action
void WifiHotspotServerSocket::SocketErrorNotice(absl::string_view reason) {
NEARBY_LOGS(WARNING) << "socket error. " << reason
<< " failed with error: " << WSAGetLastError();
closesocket(socket_to_close);
for (auto &it : socket_events_) {
if (it != WSA_INVALID_EVENT) {
WSACloseEvent(it);
it = WSA_INVALID_EVENT;
}
}
closesocket(listen_socket_);
WSACleanup();
}
@@ -293,7 +307,7 @@ bool WifiHotspotServerSocket::SetupServerSocketWinSock() {
sizeof(flag));
if (bind(listen_socket_, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) ==
SOCKET_ERROR) {
SocketErrorNotice(listen_socket_, "Bind");
SocketErrorNotice("Bind");
return false;
}
NEARBY_LOGS(INFO) << "Bind socket successful";
@@ -302,50 +316,63 @@ bool WifiHotspotServerSocket::SetupServerSocketWinSock() {
memset(&serv_addr, 0, size);
if (getsockname(listen_socket_, (struct sockaddr *)&serv_addr, &size) ==
SOCKET_ERROR) {
SocketErrorNotice(listen_socket_, "Getsockname");
SocketErrorNotice("Getsockname");
return false;
}
port_ = ntohs(serv_addr.sin_port);
NEARBY_LOGS(INFO) << "Hotspot Server bound to port: " << port_;
socket_event = WSACreateEvent();
if (socket_event == nullptr) {
SocketErrorNotice(listen_socket_, "WSACreateEvent");
socket_events_[kSocketEventListen] = WSACreateEvent();
if (socket_events_[kSocketEventListen] == WSA_INVALID_EVENT) {
SocketErrorNotice("WSACreateEvent");
return false;
}
socket_events_[kSocketEventClose] = WSACreateEvent();
if (socket_events_[kSocketEventClose] == WSA_INVALID_EVENT) {
SocketErrorNotice("WSACreateEvent");
return false;
}
// Associate event types FD_ACCEPT and FD_CLOSE with the listen_socket_ and
// socket_event
if (WSAEventSelect(listen_socket_, socket_event, FD_ACCEPT | FD_CLOSE) ==
SOCKET_ERROR) {
SocketErrorNotice(listen_socket_, "WSAEventSelect");
if (WSAEventSelect(listen_socket_, socket_events_[kSocketEventListen],
FD_ACCEPT | FD_CLOSE) == SOCKET_ERROR) {
SocketErrorNotice("WSAEventSelect");
return false;
}
if (::listen(listen_socket_, SOMAXCONN) == SOCKET_ERROR) {
SocketErrorNotice(listen_socket_, "Listen");
SocketErrorNotice("Listen");
return false;
}
NEARBY_LOGS(INFO) << "Hotspot Server Socket " << listen_socket_
<< " started to listen with socket event: " << socket_event;
submittable_executor_.Execute([this, socket_event]() {
submittable_executor_.Execute([this]() {
DWORD index;
WSANETWORKEVENTS network_events;
// Wait for network events on all sockets
index =
WSAWaitForMultipleEvents(1, &socket_event, FALSE, WSA_INFINITE, FALSE);
index = WSAWaitForMultipleEvents(kSocketEventsCount, socket_events_, FALSE,
WSA_INFINITE, FALSE);
NEARBY_LOGS(INFO) << "Hotspot Server Socket " << listen_socket_
<< " received event: " << socket_event;
<< " received event index: " << index;
if (index == WSA_WAIT_TIMEOUT || index == WSA_WAIT_FAILED) {
NEARBY_LOGS(INFO) << "Hotspot Server Socket timout or failed ";
return false;
}
index = index - WSA_WAIT_EVENT_0;
if (index == kSocketEventClose) {
// the socket is closed by SDK
NEARBY_LOGS(INFO) << "listner socket is closed.";
return false;
}
// Iterate through all events and enumerate
if (WSAEnumNetworkEvents(listen_socket_, socket_event, &network_events) ==
SOCKET_ERROR) {
if (WSAEnumNetworkEvents(listen_socket_, socket_events_[index],
&network_events) == SOCKET_ERROR) {
NEARBY_LOGS(INFO) << "Iterate through all events failed";
return false;
}
@@ -361,7 +388,8 @@ bool WifiHotspotServerSocket::SetupServerSocketWinSock() {
return false;
}
if (WSAEventSelect(listen_socket_, socket_event, 0) == SOCKET_ERROR) {
if (WSAEventSelect(listen_socket_, socket_events_[kSocketEventListen],
0) == SOCKET_ERROR) {
NEARBY_LOGS(WARNING)
<< "Remove association between listen_socket_ and event failed: "
<< WSAGetLastError();