From c00d7ae4855220a7d6a5b20447d8e46e7048dbab Mon Sep 17 00:00:00 2001 From: Edwin Wu Date: Tue, 10 Mar 2026 17:55:16 -0700 Subject: [PATCH] Implement GNCPeripheralManagerMultiplexer. PiperOrigin-RevId: 881707256 --- .../implementation/apple/Mediums/BLE/BUILD | 2 + .../BLE/GNCPeripheralManagerMultiplexer.h | 49 ++ .../BLE/GNCPeripheralManagerMultiplexer.m | 194 +++++++ .../apple/Mediums/BLE/Tests/BUILD | 1 + .../GNCPeripheralManagerMultiplexerTest.m | 495 ++++++++++++++++++ 5 files changed, 741 insertions(+) create mode 100644 internal/platform/implementation/apple/Mediums/BLE/GNCPeripheralManagerMultiplexer.h create mode 100644 internal/platform/implementation/apple/Mediums/BLE/GNCPeripheralManagerMultiplexer.m create mode 100644 internal/platform/implementation/apple/Mediums/BLE/Tests/GNCPeripheralManagerMultiplexerTest.m diff --git a/internal/platform/implementation/apple/Mediums/BLE/BUILD b/internal/platform/implementation/apple/Mediums/BLE/BUILD index 2a7a19f8..8f43a4e3 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/BUILD +++ b/internal/platform/implementation/apple/Mediums/BLE/BUILD @@ -39,6 +39,7 @@ objc_library( "GNCMConnection.m", "GNCPeripheral.m", "GNCPeripheralManager.m", + "GNCPeripheralManagerMultiplexer.m", "NSData+GNCBase85.mm", "NSData+GNCWebSafeBase64.m", ], @@ -59,6 +60,7 @@ objc_library( "GNCMConnection.h", "GNCPeripheral.h", "GNCPeripheralManager.h", + "GNCPeripheralManagerMultiplexer.h", "NSData+GNCBase85.h", "NSData+GNCWebSafeBase64.h", ], diff --git a/internal/platform/implementation/apple/Mediums/BLE/GNCPeripheralManagerMultiplexer.h b/internal/platform/implementation/apple/Mediums/BLE/GNCPeripheralManagerMultiplexer.h new file mode 100644 index 00000000..74129d02 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLE/GNCPeripheralManagerMultiplexer.h @@ -0,0 +1,49 @@ +// Copyright 2026 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/GNCPeripheralManager.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * A multiplexer that forwards @c CBPeripheralManagerDelegate and @c GNCPeripheralManagerDelegate + */ +@interface GNCPeripheralManagerMultiplexer : NSObject + +/** + * Initializes the multiplexer. + * + * @param callbackQueue The queue to use for forwarding delegate callbacks. + */ +- (instancetype)initWithCallbackQueue:(dispatch_queue_t)callbackQueue NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Adds a listener to the multiplexer. Listeners are held weakly. + * + * @param listener The listener to add. + */ +- (void)addListener:(id)listener; + +/** + * Removes a listener from the multiplexer. + * + * @param listener The listener to remove. + */ +- (void)removeListener:(id)listener; + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLE/GNCPeripheralManagerMultiplexer.m b/internal/platform/implementation/apple/Mediums/BLE/GNCPeripheralManagerMultiplexer.m new file mode 100644 index 00000000..56d69774 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLE/GNCPeripheralManagerMultiplexer.m @@ -0,0 +1,194 @@ +// Copyright 2026 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/GNCPeripheralManagerMultiplexer.h" + +#import +#import + +#import "internal/platform/implementation/apple/Mediums/BLE/GNCPeripheralManager.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation GNCPeripheralManagerMultiplexer { + NSHashTable> *_listeners; + dispatch_queue_t _callbackQueue; + dispatch_queue_t _syncQueue; +} + +- (instancetype)initWithCallbackQueue:(dispatch_queue_t)callbackQueue { + self = [super init]; + if (self) { + _listeners = [NSHashTable weakObjectsHashTable]; + _callbackQueue = callbackQueue; + _syncQueue = dispatch_queue_create("com.google.nearby.GNCPeripheralManagerMultiplexerSync", + DISPATCH_QUEUE_SERIAL); + } + return self; +} + +- (instancetype)init { + return [self initWithCallbackQueue:dispatch_get_main_queue()]; +} + +- (void)addListener:(id)listener { + dispatch_async(_syncQueue, ^{ + [self->_listeners addObject:listener]; + }); +} + +- (void)removeListener:(id)listener { + dispatch_async(_syncQueue, ^{ + [self->_listeners removeObject:listener]; + }); +} + +- (NSArray> *)allListeners { + __block NSArray> *listeners; + dispatch_sync(_syncQueue, ^{ + listeners = [self->_listeners allObjects]; + }); + return listeners; +} + +#pragma mark - GNCPeripheralManagerDelegate + +- (void)gnc_peripheralManagerDidUpdateState:(id)peripheral { + NSArray> *listeners = [self allListeners]; + dispatch_async(_callbackQueue, ^{ + for (id listener in listeners) { + [listener gnc_peripheralManagerDidUpdateState:peripheral]; + } + }); +} + +- (void)gnc_peripheralManagerDidStartAdvertising:(id)peripheral + error:(nullable NSError *)error { + NSArray> *listeners = [self allListeners]; + dispatch_async(_callbackQueue, ^{ + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(gnc_peripheralManagerDidStartAdvertising:error:)]) { + [listener gnc_peripheralManagerDidStartAdvertising:peripheral error:error]; + } + } + }); +} + +- (void)gnc_peripheralManager:(id)peripheral + didAddService:(CBService *)service + error:(nullable NSError *)error { + NSArray> *listeners = [self allListeners]; + dispatch_async(_callbackQueue, ^{ + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(gnc_peripheralManager:didAddService:error:)]) { + [listener gnc_peripheralManager:peripheral didAddService:service error:error]; + } + } + }); +} + +- (void)gnc_peripheralManager:(id)peripheral + didReceiveReadRequest:(CBATTRequest *)request { + NSArray> *listeners = [self allListeners]; + dispatch_async(_callbackQueue, ^{ + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(gnc_peripheralManager:didReceiveReadRequest:)]) { + [listener gnc_peripheralManager:peripheral didReceiveReadRequest:request]; + } + } + }); +} + +- (void)gnc_peripheralManager:(id)peripheral + didPublishL2CAPChannel:(CBL2CAPPSM)PSM + error:(nullable NSError *)error { + NSArray> *listeners = [self allListeners]; + dispatch_async(_callbackQueue, ^{ + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(gnc_peripheralManager:didPublishL2CAPChannel:error:)]) { + [listener gnc_peripheralManager:peripheral didPublishL2CAPChannel:PSM error:error]; + } + } + }); +} + +- (void)gnc_peripheralManager:(id)peripheral + didUnpublishL2CAPChannel:(CBL2CAPPSM)PSM + error:(NSError *)error { + NSArray> *listeners = [self allListeners]; + dispatch_async(_callbackQueue, ^{ + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(gnc_peripheralManager:didUnpublishL2CAPChannel:error:)]) { + [listener gnc_peripheralManager:peripheral didUnpublishL2CAPChannel:PSM error:error]; + } + } + }); +} + +- (void)gnc_peripheralManager:(id)peripheral + didOpenL2CAPChannel:(nullable CBL2CAPChannel *)channel + error:(nullable NSError *)error { + NSArray> *listeners = [self allListeners]; + dispatch_async(_callbackQueue, ^{ + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(gnc_peripheralManager:didOpenL2CAPChannel:error:)]) { + [listener gnc_peripheralManager:peripheral didOpenL2CAPChannel:channel error:error]; + } + } + }); +} + +#pragma mark - CBPeripheralManagerDelegate + +- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral { + [self gnc_peripheralManagerDidUpdateState:peripheral]; +} + +- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral + error:(nullable NSError *)error { + [self gnc_peripheralManagerDidStartAdvertising:peripheral error:error]; +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didAddService:(CBService *)service + error:(nullable NSError *)error { + [self gnc_peripheralManager:peripheral didAddService:service error:error]; +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didReceiveReadRequest:(CBATTRequest *)request { + [self gnc_peripheralManager:peripheral didReceiveReadRequest:request]; +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didPublishL2CAPChannel:(CBL2CAPPSM)PSM + error:(nullable NSError *)error { + [self gnc_peripheralManager:peripheral didPublishL2CAPChannel:PSM error:error]; +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didUnpublishL2CAPChannel:(CBL2CAPPSM)PSM + error:(nullable NSError *)error { + [self gnc_peripheralManager:peripheral didUnpublishL2CAPChannel:PSM error:error]; +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didOpenL2CAPChannel:(nullable CBL2CAPChannel *)channel + error:(nullable NSError *)error { + [self gnc_peripheralManager:peripheral didOpenL2CAPChannel:channel error:error]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD b/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD index 86e1be56..245bf027 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD @@ -44,6 +44,7 @@ objc_library( "GNCMBleUtilsTest.m", "GNCMConnectionsTest.m", "GNCMFakeConnection.mm", + "GNCPeripheralManagerMultiplexerTest.m", "GNCPeripheralManagerTest.m", "GNCPeripheralTest.m", "NSData+GNCBase85Test.m", diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCPeripheralManagerMultiplexerTest.m b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCPeripheralManagerMultiplexerTest.m new file mode 100644 index 00000000..394eb4cc --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCPeripheralManagerMultiplexerTest.m @@ -0,0 +1,495 @@ +// Copyright 2026 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/GNCPeripheralManagerMultiplexer.h" + +#import +#import +#import + +#import "internal/platform/implementation/apple/Mediums/BLE/GNCPeripheralManager.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.h" + +@interface GNCPeripheralManagerMultiplexerTest : XCTestCase +@end + +@interface FakePeripheralManagerDelegate : NSObject +@property(nonatomic) XCTestExpectation *expectation; +@property(nonatomic) BOOL didUpdateStateCalled; +@property(nonatomic) CBManagerState state; + +@property(nonatomic) BOOL didStartAdvertisingCalled; +@property(nonatomic) NSError *startAdvertisingError; + +@property(nonatomic) BOOL didAddServiceCalled; +@property(nonatomic) CBService *addedService; +@property(nonatomic) NSError *addServiceError; + +@property(nonatomic) BOOL didReceiveReadRequestCalled; +@property(nonatomic) CBATTRequest *readRequest; + +@property(nonatomic) BOOL didPublishL2CAPChannelCalled; +@property(nonatomic) CBL2CAPPSM publishedPSM; +@property(nonatomic) NSError *publishL2CAPChannelError; + +@property(nonatomic) BOOL didUnpublishL2CAPChannelCalled; +@property(nonatomic) CBL2CAPPSM unpublishedPSM; +@property(nonatomic) NSError *unpublishL2CAPChannelError; + +@property(nonatomic) BOOL didOpenL2CAPChannelCalled; +@property(nonatomic) CBL2CAPChannel *openedChannel; +@property(nonatomic) NSError *openL2CAPChannelError; + +@end + +@implementation FakePeripheralManagerDelegate + +- (void)gnc_peripheralManagerDidUpdateState:(id)peripheral { + _didUpdateStateCalled = YES; + _state = peripheral.state; + if (_expectation) { + [_expectation fulfill]; + } +} + +- (void)gnc_peripheralManagerDidStartAdvertising:(id)peripheral + error:(nullable NSError *)error { + _didStartAdvertisingCalled = YES; + _startAdvertisingError = error; + if (_expectation) { + [_expectation fulfill]; + } +} + +- (void)gnc_peripheralManager:(id)peripheral + didAddService:(CBService *)service + error:(nullable NSError *)error { + _didAddServiceCalled = YES; + _addedService = service; + _addServiceError = error; + if (_expectation) { + [_expectation fulfill]; + } +} + +- (void)gnc_peripheralManager:(id)peripheral + didReceiveReadRequest:(CBATTRequest *)request { + _didReceiveReadRequestCalled = YES; + _readRequest = request; + if (_expectation) { + [_expectation fulfill]; + } +} + +- (void)gnc_peripheralManager:(id)peripheral + didPublishL2CAPChannel:(CBL2CAPPSM)PSM + error:(nullable NSError *)error { + _didPublishL2CAPChannelCalled = YES; + _publishedPSM = PSM; + _publishL2CAPChannelError = error; + if (_expectation) { + [_expectation fulfill]; + } +} + +- (void)gnc_peripheralManager:(id)peripheral + didUnpublishL2CAPChannel:(CBL2CAPPSM)PSM + error:(NSError *)error { + _didUnpublishL2CAPChannelCalled = YES; + _unpublishedPSM = PSM; + _unpublishL2CAPChannelError = error; + if (_expectation) { + [_expectation fulfill]; + } +} + +- (void)gnc_peripheralManager:(id)peripheral + didOpenL2CAPChannel:(nullable CBL2CAPChannel *)channel + error:(nullable NSError *)error { + _didOpenL2CAPChannelCalled = YES; + _openedChannel = channel; + _openL2CAPChannelError = error; + if (_expectation) { + [_expectation fulfill]; + } +} + +- (void)peripheralManagerDidUpdateState:(nonnull CBPeripheralManager *)peripheral { + _didUpdateStateCalled = YES; + _state = peripheral.state; + if (_expectation) { + [_expectation fulfill]; + } +} + +- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral + error:(nullable NSError *)error { + _didStartAdvertisingCalled = YES; + _startAdvertisingError = error; + if (_expectation) { + [_expectation fulfill]; + } +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didAddService:(CBService *)service + error:(nullable NSError *)error { + _didAddServiceCalled = YES; + _addedService = service; + _addServiceError = error; + if (_expectation) { + [_expectation fulfill]; + } +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didReceiveReadRequest:(CBATTRequest *)request { + _didReceiveReadRequestCalled = YES; + _readRequest = request; + if (_expectation) { + [_expectation fulfill]; + } +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didPublishL2CAPChannel:(CBL2CAPPSM)PSM + error:(nullable NSError *)error { + _didPublishL2CAPChannelCalled = YES; + _publishedPSM = PSM; + _publishL2CAPChannelError = error; + if (_expectation) { + [_expectation fulfill]; + } +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didUnpublishL2CAPChannel:(CBL2CAPPSM)PSM + error:(nullable NSError *)error { + _didUnpublishL2CAPChannelCalled = YES; + _unpublishedPSM = PSM; + _unpublishL2CAPChannelError = error; + if (_expectation) { + [_expectation fulfill]; + } +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didOpenL2CAPChannel:(nullable CBL2CAPChannel *)channel + error:(nullable NSError *)error { + _didOpenL2CAPChannelCalled = YES; + _openedChannel = channel; + _openL2CAPChannelError = error; + if (_expectation) { + [_expectation fulfill]; + } +} + +@end + +@implementation GNCPeripheralManagerMultiplexerTest + +- (void)testMultiplexerForwardsCallbacks { + GNCPeripheralManagerMultiplexer *multiplexer = + [[GNCPeripheralManagerMultiplexer alloc] initWithCallbackQueue:dispatch_get_main_queue()]; + FakePeripheralManagerDelegate *delegate1 = [[FakePeripheralManagerDelegate alloc] init]; + FakePeripheralManagerDelegate *delegate2 = [[FakePeripheralManagerDelegate alloc] init]; + + delegate1.expectation = [self expectationWithDescription:@"Delegate 1 called"]; + delegate2.expectation = [self expectationWithDescription:@"Delegate 2 called"]; + + [multiplexer addListener:delegate1]; + [multiplexer addListener:delegate2]; + + GNCFakePeripheralManager *fakeManager = [[GNCFakePeripheralManager alloc] init]; + fakeManager.state = CBManagerStatePoweredOn; + + [multiplexer gnc_peripheralManagerDidUpdateState:fakeManager]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + + XCTAssertTrue(delegate1.didUpdateStateCalled); + XCTAssertTrue(delegate2.didUpdateStateCalled); + XCTAssertEqual(delegate1.state, CBManagerStatePoweredOn); + XCTAssertEqual(delegate2.state, CBManagerStatePoweredOn); +} + +- (void)testMultiplexerRemovesListener { + GNCPeripheralManagerMultiplexer *multiplexer = + [[GNCPeripheralManagerMultiplexer alloc] initWithCallbackQueue:dispatch_get_main_queue()]; + FakePeripheralManagerDelegate *delegate1 = [[FakePeripheralManagerDelegate alloc] init]; + + delegate1.expectation = [self expectationWithDescription:@"Delegate 1 called"]; + + [multiplexer addListener:delegate1]; + + GNCFakePeripheralManager *fakeManager = [[GNCFakePeripheralManager alloc] init]; + fakeManager.state = CBManagerStatePoweredOn; + + [multiplexer removeListener:delegate1]; + [multiplexer gnc_peripheralManagerDidUpdateState:fakeManager]; + + // We expect delegate1 NOT to be called. + // Since removals are async, we wait a bit to ensure it had a chance (or didn't). + XCTWaiterResult result = [XCTWaiter waitForExpectations:@[ delegate1.expectation ] timeout:0.5]; + XCTAssertEqual(result, XCTWaiterResultTimedOut); + XCTAssertFalse(delegate1.didUpdateStateCalled); +} + +- (void)testMultiplexerForwardsDidStartAdvertising { + GNCPeripheralManagerMultiplexer *multiplexer = + [[GNCPeripheralManagerMultiplexer alloc] initWithCallbackQueue:dispatch_get_main_queue()]; + FakePeripheralManagerDelegate *delegate = [[FakePeripheralManagerDelegate alloc] init]; + delegate.expectation = [self expectationWithDescription:@"Delegate called"]; + [multiplexer addListener:delegate]; + + GNCFakePeripheralManager *fakeManager = [[GNCFakePeripheralManager alloc] init]; + NSError *error = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + + [multiplexer gnc_peripheralManagerDidStartAdvertising:fakeManager error:error]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(delegate.didStartAdvertisingCalled); + XCTAssertEqualObjects(delegate.startAdvertisingError, error); +} + +- (void)testMultiplexerForwardsDidAddService { + GNCPeripheralManagerMultiplexer *multiplexer = + [[GNCPeripheralManagerMultiplexer alloc] initWithCallbackQueue:dispatch_get_main_queue()]; + FakePeripheralManagerDelegate *delegate = [[FakePeripheralManagerDelegate alloc] init]; + delegate.expectation = [self expectationWithDescription:@"Delegate called"]; + [multiplexer addListener:delegate]; + + GNCFakePeripheralManager *fakeManager = [[GNCFakePeripheralManager alloc] init]; + CBMutableService *service = + [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:@"180D"] primary:YES]; + NSError *error = [NSError errorWithDomain:@"test" code:2 userInfo:nil]; + + [multiplexer gnc_peripheralManager:fakeManager didAddService:service error:error]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(delegate.didAddServiceCalled); + XCTAssertEqualObjects(delegate.addedService, service); + XCTAssertEqualObjects(delegate.addServiceError, error); +} + +- (void)testMultiplexerForwardsDidReceiveReadRequest { + GNCPeripheralManagerMultiplexer *multiplexer = + [[GNCPeripheralManagerMultiplexer alloc] initWithCallbackQueue:dispatch_get_main_queue()]; + FakePeripheralManagerDelegate *delegate = [[FakePeripheralManagerDelegate alloc] init]; + delegate.expectation = [self expectationWithDescription:@"Delegate called"]; + [multiplexer addListener:delegate]; + + GNCFakePeripheralManager *fakeManager = [[GNCFakePeripheralManager alloc] init]; + id request = [NSNull null]; // Use NSNull or any object as placeholder since we can't create + // CBATTRequest + + [multiplexer gnc_peripheralManager:fakeManager didReceiveReadRequest:request]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(delegate.didReceiveReadRequestCalled); + XCTAssertEqual(delegate.readRequest, request); +} + +- (void)testMultiplexerForwardsDidPublishL2CAPChannel { + GNCPeripheralManagerMultiplexer *multiplexer = + [[GNCPeripheralManagerMultiplexer alloc] initWithCallbackQueue:dispatch_get_main_queue()]; + FakePeripheralManagerDelegate *delegate = [[FakePeripheralManagerDelegate alloc] init]; + delegate.expectation = [self expectationWithDescription:@"Delegate called"]; + [multiplexer addListener:delegate]; + + GNCFakePeripheralManager *fakeManager = [[GNCFakePeripheralManager alloc] init]; + CBL2CAPPSM psm = 42; + NSError *error = [NSError errorWithDomain:@"test" code:3 userInfo:nil]; + + [multiplexer gnc_peripheralManager:fakeManager didPublishL2CAPChannel:psm error:error]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(delegate.didPublishL2CAPChannelCalled); + XCTAssertEqual(delegate.publishedPSM, psm); + XCTAssertEqualObjects(delegate.publishL2CAPChannelError, error); +} + +- (void)testMultiplexerForwardsDidUnpublishL2CAPChannel { + GNCPeripheralManagerMultiplexer *multiplexer = + [[GNCPeripheralManagerMultiplexer alloc] initWithCallbackQueue:dispatch_get_main_queue()]; + FakePeripheralManagerDelegate *delegate = [[FakePeripheralManagerDelegate alloc] init]; + delegate.expectation = [self expectationWithDescription:@"Delegate called"]; + [multiplexer addListener:delegate]; + + GNCFakePeripheralManager *fakeManager = [[GNCFakePeripheralManager alloc] init]; + CBL2CAPPSM psm = 42; + NSError *error = [NSError errorWithDomain:@"test" code:4 userInfo:nil]; + + [multiplexer gnc_peripheralManager:fakeManager didUnpublishL2CAPChannel:psm error:error]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(delegate.didUnpublishL2CAPChannelCalled); + XCTAssertEqual(delegate.unpublishedPSM, psm); + XCTAssertEqualObjects(delegate.unpublishL2CAPChannelError, error); +} + +- (void)testMultiplexerForwardsDidOpenL2CAPChannel { + GNCPeripheralManagerMultiplexer *multiplexer = + [[GNCPeripheralManagerMultiplexer alloc] initWithCallbackQueue:dispatch_get_main_queue()]; + FakePeripheralManagerDelegate *delegate = [[FakePeripheralManagerDelegate alloc] init]; + delegate.expectation = [self expectationWithDescription:@"Delegate called"]; + [multiplexer addListener:delegate]; + + GNCFakePeripheralManager *fakeManager = [[GNCFakePeripheralManager alloc] init]; + id channel = [NSNull null]; // Placeholder + NSError *error = [NSError errorWithDomain:@"test" code:5 userInfo:nil]; + + [multiplexer gnc_peripheralManager:fakeManager didOpenL2CAPChannel:channel error:error]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(delegate.didOpenL2CAPChannelCalled); + XCTAssertEqual(delegate.openedChannel, channel); + XCTAssertEqualObjects(delegate.openL2CAPChannelError, error); +} + +- (void)testCBPeripheralManagerDelegateDidUpdateState { + GNCPeripheralManagerMultiplexer *multiplexer = + [[GNCPeripheralManagerMultiplexer alloc] initWithCallbackQueue:dispatch_get_main_queue()]; + FakePeripheralManagerDelegate *delegate = [[FakePeripheralManagerDelegate alloc] init]; + delegate.expectation = [self expectationWithDescription:@"Delegate called"]; + [multiplexer addListener:delegate]; + + GNCFakePeripheralManager *fakeManager = [[GNCFakePeripheralManager alloc] init]; + fakeManager.state = CBManagerStatePoweredOn; + + [multiplexer peripheralManagerDidUpdateState:(CBPeripheralManager *)fakeManager]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(delegate.didUpdateStateCalled); + XCTAssertEqual(delegate.state, CBManagerStatePoweredOn); +} + +- (void)testCBPeripheralManagerDelegateDidStartAdvertising { + GNCPeripheralManagerMultiplexer *multiplexer = + [[GNCPeripheralManagerMultiplexer alloc] initWithCallbackQueue:dispatch_get_main_queue()]; + FakePeripheralManagerDelegate *delegate = [[FakePeripheralManagerDelegate alloc] init]; + delegate.expectation = [self expectationWithDescription:@"Delegate called"]; + [multiplexer addListener:delegate]; + + GNCFakePeripheralManager *fakeManager = [[GNCFakePeripheralManager alloc] init]; + NSError *error = [NSError errorWithDomain:@"test" code:10 userInfo:nil]; + + [multiplexer peripheralManagerDidStartAdvertising:(CBPeripheralManager *)fakeManager error:error]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(delegate.didStartAdvertisingCalled); + XCTAssertEqualObjects(delegate.startAdvertisingError, error); +} + +- (void)testCBPeripheralManagerDelegateDidAddService { + GNCPeripheralManagerMultiplexer *multiplexer = + [[GNCPeripheralManagerMultiplexer alloc] initWithCallbackQueue:dispatch_get_main_queue()]; + FakePeripheralManagerDelegate *delegate = [[FakePeripheralManagerDelegate alloc] init]; + delegate.expectation = [self expectationWithDescription:@"Delegate called"]; + [multiplexer addListener:delegate]; + + GNCFakePeripheralManager *fakeManager = [[GNCFakePeripheralManager alloc] init]; + CBMutableService *service = + [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:@"180F"] primary:YES]; + NSError *error = [NSError errorWithDomain:@"test" code:11 userInfo:nil]; + + [multiplexer peripheralManager:(CBPeripheralManager *)fakeManager didAddService:service error:error]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(delegate.didAddServiceCalled); + XCTAssertEqualObjects(delegate.addedService, service); + XCTAssertEqualObjects(delegate.addServiceError, error); +} + +- (void)testCBPeripheralManagerDelegateDidReceiveReadRequest { + GNCPeripheralManagerMultiplexer *multiplexer = + [[GNCPeripheralManagerMultiplexer alloc] initWithCallbackQueue:dispatch_get_main_queue()]; + FakePeripheralManagerDelegate *delegate = [[FakePeripheralManagerDelegate alloc] init]; + delegate.expectation = [self expectationWithDescription:@"Delegate called"]; + [multiplexer addListener:delegate]; + + GNCFakePeripheralManager *fakeManager = [[GNCFakePeripheralManager alloc] init]; + id request = [NSNull null]; + + [multiplexer peripheralManager:(CBPeripheralManager *)fakeManager didReceiveReadRequest:request]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(delegate.didReceiveReadRequestCalled); + XCTAssertEqual(delegate.readRequest, request); +} + +- (void)testCBPeripheralManagerDelegateDidPublishL2CAPChannel { + GNCPeripheralManagerMultiplexer *multiplexer = + [[GNCPeripheralManagerMultiplexer alloc] initWithCallbackQueue:dispatch_get_main_queue()]; + FakePeripheralManagerDelegate *delegate = [[FakePeripheralManagerDelegate alloc] init]; + delegate.expectation = [self expectationWithDescription:@"Delegate called"]; + [multiplexer addListener:delegate]; + + GNCFakePeripheralManager *fakeManager = [[GNCFakePeripheralManager alloc] init]; + CBL2CAPPSM psm = 100; + NSError *error = [NSError errorWithDomain:@"test" code:13 userInfo:nil]; + + [multiplexer peripheralManager:(CBPeripheralManager *)fakeManager + didPublishL2CAPChannel:psm + error:error]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(delegate.didPublishL2CAPChannelCalled); + XCTAssertEqual(delegate.publishedPSM, psm); + XCTAssertEqualObjects(delegate.publishL2CAPChannelError, error); +} + +- (void)testCBPeripheralManagerDelegateDidUnpublishL2CAPChannel { + GNCPeripheralManagerMultiplexer *multiplexer = + [[GNCPeripheralManagerMultiplexer alloc] initWithCallbackQueue:dispatch_get_main_queue()]; + FakePeripheralManagerDelegate *delegate = [[FakePeripheralManagerDelegate alloc] init]; + delegate.expectation = [self expectationWithDescription:@"Delegate called"]; + [multiplexer addListener:delegate]; + + GNCFakePeripheralManager *fakeManager = [[GNCFakePeripheralManager alloc] init]; + CBL2CAPPSM psm = 101; + NSError *error = [NSError errorWithDomain:@"test" code:14 userInfo:nil]; + + [multiplexer peripheralManager:(CBPeripheralManager *)fakeManager + didUnpublishL2CAPChannel:psm + error:error]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(delegate.didUnpublishL2CAPChannelCalled); + XCTAssertEqual(delegate.unpublishedPSM, psm); + XCTAssertEqualObjects(delegate.unpublishL2CAPChannelError, error); +} + +- (void)testCBPeripheralManagerDelegateDidOpenL2CAPChannel { + GNCPeripheralManagerMultiplexer *multiplexer = + [[GNCPeripheralManagerMultiplexer alloc] initWithCallbackQueue:dispatch_get_main_queue()]; + FakePeripheralManagerDelegate *delegate = [[FakePeripheralManagerDelegate alloc] init]; + delegate.expectation = [self expectationWithDescription:@"Delegate called"]; + [multiplexer addListener:delegate]; + + GNCFakePeripheralManager *fakeManager = [[GNCFakePeripheralManager alloc] init]; + id channel = [NSNull null]; + NSError *error = [NSError errorWithDomain:@"test" code:15 userInfo:nil]; + + [multiplexer peripheralManager:(CBPeripheralManager *)fakeManager + didOpenL2CAPChannel:channel + error:error]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(delegate.didOpenL2CAPChannelCalled); + XCTAssertEqual(delegate.openedChannel, channel); + XCTAssertEqualObjects(delegate.openL2CAPChannelError, error); +} + +@end