diff --git a/connections/clients/swift/NearbyConnections/Sources/Advertiser.swift b/connections/clients/swift/NearbyConnections/Sources/Advertiser.swift index db2c442d..9786e023 100644 --- a/connections/clients/swift/NearbyConnections/Sources/Advertiser.swift +++ b/connections/clients/swift/NearbyConnections/Sources/Advertiser.swift @@ -12,6 +12,135 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Foundation import NearbyCoreAdapter -public class Advertiser {} +/// Publishes an advertisement and notifies its delegate about connection requests from nearby +/// endpoints. +public class Advertiser { + + /// The delegate object that handles advertising-related events. + public weak var delegate: AdvertiserDelegate? + + /// The connection manager for this instance. + public let connectionManager: ConnectionManager + + lazy var connection: InternalConnection? = { + let connection = InternalConnection() + connection.delegate = self + return connection + }() + + /// Initializes the discoverer object. + /// + /// - Parameter connectionManager: The connection manager for this instance. + public init(connectionManager: ConnectionManager) { + self.connectionManager = connectionManager + } + + /// Starts advertising the local endpoint. + /// + /// After this method is called (until you call `stopAdvertising()`), the framework calls your + /// delegate’s `advertiser(_:didReceiveConnectionRequestFrom:with:connectionRequestHandler:)` + /// method when remote endpoints request a connection. + public func startAdvertising(using context: Data) { + let options = GNCAdvertisingOptions(strategy: connectionManager.strategy.objc) + // TODO(b/257824412): Handle errors from completion handler. + GNCCoreAdapter.shared.startAdvertising( + asService: connectionManager.serviceID, endpointInfo: context, + options: options, delegate: connection) + } + + /// Stops advertising the local endpoint. + public func stopAdvertising() { + // TODO(b/257824412): Handle errors from completion handler. + GNCCoreAdapter.shared.stopAdvertising() + } + + deinit { + stopAdvertising() + } +} + +extension Advertiser: InternalConnectionDelegate { + + func connected( + toEndpoint endpointID: String, withEndpointInfo info: Data, authenticationToken: String + ) { + connectionManager.queue.async { [weak self] in + guard let self = self else { return } + self.delegate?.advertiser(self, didReceiveConnectionRequestFrom: endpointID, with: info) { + (accept) in + guard accept else { + // TODO(b/257824412): Handle errors from completion handler. + GNCCoreAdapter.shared.rejectConnectionRequest(fromEndpoint: endpointID) + return + } + self.connectionManager.delegate?.connectionManager( + self.connectionManager, didChangeTo: .connecting, for: endpointID) + self.connectionManager.delegate?.connectionManager( + self.connectionManager, didReceive: authenticationToken, from: endpointID + ) { accept in + guard accept else { + // TODO(b/257824412): Handle errors from completion handler. + GNCCoreAdapter.shared.rejectConnectionRequest(fromEndpoint: endpointID) + return + } + // TODO(b/257824412): Handle errors from completion handler. + GNCCoreAdapter.shared.acceptConnectionRequest( + fromEndpoint: endpointID, delegate: self.connectionManager.payload) + + } + } + } + } + + func acceptedConnection(toEndpoint endpointID: String) { + connectionManager.queue.async { [weak self] in + guard let connectionManager = self?.connectionManager else { return } + connectionManager.delegate?.connectionManager( + connectionManager, didChangeTo: .connected, for: endpointID) + } + } + + func rejectedConnection(toEndpoint endpointID: String, with status: GNCStatus) { + connectionManager.queue.async { [weak self] in + guard let connectionManager = self?.connectionManager else { return } + connectionManager.delegate?.connectionManager( + connectionManager, didChangeTo: .rejected, for: endpointID) + } + } + + func disconnected(fromEndpoint endpointID: String) { + connectionManager.queue.async { [weak self] in + guard let connectionManager = self?.connectionManager else { return } + connectionManager.delegate?.connectionManager( + connectionManager, didChangeTo: .disconnected, for: endpointID) + } + } +} + +/// The `AdvertiserDelegate` protocol defines methods that an `Advertiser` object’s delegate can +/// implement to handle advertising-related events. +/// +/// Delegate methods are executed on the `.main` queue by default, but a specific `DispatchQueue` on +/// which to call the delegate methods can be passed to `ConnectionManager`. +public protocol AdvertiserDelegate: AnyObject { + + /// Called when a connection request is received from a nearby endpoint. + /// + /// Important: Accepting a connection request will not yet establish a connection that allows + /// data transference. The connection must first be verified by both parties via the + /// `ConnectionManagerDelegate`, before data can flow freely. + /// + /// - Parameters: + /// - advertiser: The advertiser object that received a connection request. + /// - endpointID: The endpoint ID of the nearby enpoint that requested the connection. + /// - info: An arbitrary piece of data received from the nearby endpoint. This can be used to + /// provide further information to the user about the nature of the invitation. + /// - connectionRequestHandler: A block that your code must call to indicate whether the + /// advertiser should accept or decline the connection request. + func advertiser( + _ advertiser: Advertiser, didReceiveConnectionRequestFrom endpointID: EndpointID, + with context: Data, connectionRequestHandler: @escaping (Bool) -> Void) +} diff --git a/connections/clients/swift/NearbyConnections/Sources/Discoverer.swift b/connections/clients/swift/NearbyConnections/Sources/Discoverer.swift index 6cef2fff..53d47709 100644 --- a/connections/clients/swift/NearbyConnections/Sources/Discoverer.swift +++ b/connections/clients/swift/NearbyConnections/Sources/Discoverer.swift @@ -12,6 +12,171 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Foundation import NearbyCoreAdapter -public class Discoverer {} +/// Searches for nearby endpoints and provides the ability to request connections to them. +public class Discoverer { + + /// The delegate object that handles discovery-related events. + public weak var delegate: DiscovererDelegate? + + /// The connection manager for this instance. + public let connectionManager: ConnectionManager + + lazy var connection: InternalConnection? = { + let connection = InternalConnection() + connection.delegate = self + return connection + }() + + lazy var discovery: InternalDiscovery? = { + let discovery = InternalDiscovery() + discovery.delegate = self + return discovery + }() + + /// Initializes the discoverer object. + /// + /// - Parameter connectionManager: The connection manager for this instance. + public init(connectionManager: ConnectionManager) { + self.connectionManager = connectionManager + } + + /// Starts searching for nearby remote endpoints. + /// + /// After this method is called (until you call `stopDiscovery()`), the framework calls your + /// delegate’s `discoverer(_:didFind:with:)` and `discoverer(_:didLose:)` methods as new endpoints + /// are found and lost. + public func startDiscovery() { + let options = GNCDiscoveryOptions(strategy: connectionManager.strategy.objc) + // TODO(b/257824412): Handle errors from completion handler. + GNCCoreAdapter.shared.startDiscovery( + asService: connectionManager.serviceID, options: options, delegate: discovery) + } + + /// Stops searching for nearby remote endpoints. + public func stopDiscovery() { + // TODO(b/257824412): Handle errors from completion handler. + GNCCoreAdapter.shared.stopDiscovery() + } + + /// Requests a connection to a discovered remote endpoint. + /// + /// - Parameters: + /// - endpointID: The ID of the endpoint to request a connection to. + /// - context: An arbitrary piece of data that is passed to the nearby endpoint. This can be + /// used to provide further information to the user about the nature of the invitation. + public func requestConnection(to endpointID: EndpointID, using context: Data) { + let options = GNCConnectionOptions() + // TODO(b/257824412): Handle errors from completion handler. + GNCCoreAdapter.shared.requestConnection( + toEndpoint: endpointID, endpointInfo: context, + options: options, delegate: connection) + } + + deinit { + stopDiscovery() + } +} + +extension Discoverer: InternalConnectionDelegate { + + func connected( + toEndpoint endpointID: String, withEndpointInfo info: Data, authenticationToken: String + ) { + connectionManager.queue.async { [weak self] in + guard let connectionManager = self?.connectionManager else { return } + connectionManager.delegate?.connectionManager( + connectionManager, didChangeTo: .connecting, for: endpointID) + connectionManager.delegate?.connectionManager( + connectionManager, didReceive: authenticationToken, from: endpointID + ) { accept in + guard accept else { + // TODO(b/257824412): Handle errors from completion handler. + GNCCoreAdapter.shared.rejectConnectionRequest(fromEndpoint: endpointID) + return + } + // TODO(b/257824412): Handle errors from completion handler. + GNCCoreAdapter.shared.acceptConnectionRequest( + fromEndpoint: endpointID, delegate: connectionManager.payload) + } + } + } + + func acceptedConnection(toEndpoint endpointID: String) { + connectionManager.queue.async { [weak self] in + guard let connectionManager = self?.connectionManager else { return } + connectionManager.delegate?.connectionManager( + connectionManager, didChangeTo: .connected, for: endpointID) + } + } + + func rejectedConnection(toEndpoint endpointID: String, with status: GNCStatus) { + connectionManager.queue.async { [weak self] in + guard let connectionManager = self?.connectionManager else { return } + connectionManager.delegate?.connectionManager( + connectionManager, didChangeTo: .rejected, for: endpointID) + } + } + + func disconnected(fromEndpoint endpointID: String) { + connectionManager.queue.async { [weak self] in + guard let connectionManager = self?.connectionManager else { return } + connectionManager.delegate?.connectionManager( + connectionManager, didChangeTo: .disconnected, for: endpointID) + } + } +} + +extension Discoverer: InternalDiscoveryDelegate { + + func foundEndpoint(_ endpointID: String, withEndpointInfo info: Data) { + connectionManager.queue.async { [weak self] in + guard let self = self else { return } + self.delegate?.discoverer(self, didFind: endpointID, with: info) + } + } + + func lostEndpoint(_ endpointID: String) { + connectionManager.queue.async { [weak self] in + guard let self = self else { return } + self.delegate?.discoverer(self, didLose: endpointID) + } + } +} + +/// The `DiscovererDelegate` protocol defines methods that a `Discoverer` object’s delegate can +/// implement to handle discovery-related events. +/// +/// Delegate methods are executed on the `.main` queue by default, but a specific `DispatchQueue` on +/// which to call the delegate methods can be passed to `ConnectionManager`. +public protocol DiscovererDelegate: AnyObject { + + /// Called when a nearby endpoint is found. + /// + /// The endpoint ID provided to this delegate method can be used to request a connection with the + /// nearby endpoint. + /// + /// - Parameters: + /// - discoverer: The discoverer object that found the nearby endpoint. + /// - endpointID: The unique ID of the endpoint that was found. + /// - context: An arbitrary piece of data received from the nearby endpoint. This can be used to + /// provide further information to the user about the nature of the invitation. + func discoverer(_ discoverer: Discoverer, didFind endpointID: EndpointID, with context: Data) + + /// Called when a nearby endpoint is lost. + /// + /// This callback informs your app that connection requests can no longer be sent to an endpoint, + /// and that your app should remove that endpoint from its user interface. + /// + /// Important: Because there is a delay between when a discovered endpoint disappears and when + /// Nearby Connections detects that it has left, the fact that your app has not yet received a + /// disappearance callback does not guarantee that it can communicate with the endpoint + /// successfully. + /// + /// - Parameters: + /// - discoverer: The discoverer object that lost the nearby endpoint. + /// - endpointID: The unique ID of the nearby endpoint that was lost. + func discoverer(_ discoverer: Discoverer, didLose endpointID: EndpointID) +}