Add connection manager to Swift layer

PiperOrigin-RevId: 490288395
This commit is contained in:
Nick Bourdakos
2022-11-22 11:20:27 -08:00
committed by Copybara-Service
parent 9e61866ee3
commit 808611f316
@@ -14,4 +14,362 @@
import NearbyCoreAdapter
public class ConnectionManager {}
/// Manages communication between the local endpoint and all remote endpoints connected to it.
public class ConnectionManager {
/// The delegate object that handles connection-related events.
public weak var delegate: ConnectionManagerDelegate?
/// The service identifier to advertise as or search for.
public let serviceID: String
/// A value indicating the connection strategy and restrictions for advertisement or discovery.
public let strategy: Strategy
/// `DispatchQueue` on which all delegate methods are called.
public let queue: DispatchQueue
private var transfers: [PayloadID: [EndpointID: Progress]] = [:]
lazy var payload: InternalPayload? = {
let payload = InternalPayload()
payload.delegate = self
return payload
}()
/// Initializes the connection manager object.
///
/// - Parameters:
/// - serviceID: The indentifier of your service advertised to other endpoints or to be searched
/// for while discovering other endpoints. This can be an arbitrary string, so long as it
/// uniquely identifies your service. A good default is to use your apps package name.
/// - strategy: Connection strategy to be used during advertisement or discovery.
/// - queue: `DispatchQueue` on which all delegate methods are called. `.main` by default.
public init(serviceID: String, strategy: Strategy, queue: DispatchQueue = .main) {
self.serviceID = serviceID
self.strategy = strategy
self.queue = queue
}
/// Sends a message as `Data` to nearby endpoints.
///
/// This method is asynchronous (nonblocking).
///
/// On the recipient device, the connection manager instance calls its delegates
/// `connectionManager(_:didReceive:withID:from:)` method with the message after it has been fully
/// received.
///
/// On both the local and recipient device, the connection manager instance calls its delegates
/// `connectionManager(_:didReceiveTransferUpdate:from:forPayload:)` method when delivery
/// succeeds, has a progress update or when an in-flight error occurs.
///
/// - Parameters:
/// - data: An instance containing the message to send.
/// - endpointIDs: An array of endpoint IDs representing the endpoints that should receive the
/// message.
/// - id: A unique ID for the payload. This ID is provided to the remote endpoint. A random ID
/// will be generated if none is set.
/// - completionHandler: A closure that gets called when the framework attempts to send the
/// message. Upon success, the handler is called with an error value of nil. Upon failure,
/// the handle is called with an error object that indicates what went wrong. This does not
/// indicate that the send has successfully completed yet. Errors might still occur during
/// transmission (and at different times for different endpoints).
/// `connectionManager(_:didReceiveTransferUpdate:from:forPayload:)`is called when delivery
/// succeeds or when an in-flight error occurs.
/// - Returns: A cancellation token used to cancel the transfer.
public func send(
_ data: Data, to endpointIDs: [EndpointID], id: PayloadID? = nil,
completionHandler: ((Error?) -> Void)? = nil
)
-> CancellationToken
{
let payload: GNCBytesPayload
if let id = id {
payload = GNCBytesPayload(data: data, identifier: id)
} else {
payload = GNCBytesPayload(data: data)
}
return send(payload, to: endpointIDs, completionHandler: completionHandler)
}
/// Opens a byte stream to nearby endpoints.
///
/// This method is asynchronous (nonblocking).
///
/// On the recipient device, the connection manager instance calls its delegates
/// `connectionManager(_:didReceive:withID:from:cancellationToken:)` method with the stream.
///
/// On both the local and recipient device, the connection manager instance calls its delegates
/// `connectionManager(_:didReceiveTransferUpdate:from:forPayload:)` method when delivery
/// succeeds, has a progress update or when an in-flight error occurs.
///
/// - Parameters:
/// - stream:
/// - endpointIDs: An array of endpoint IDs representing the endpoints that should receive this
/// stream.
/// - id: A unique ID for the payload. This ID is provided to the remote endpoint. A random ID
/// will be generated if none is set.
/// - completionHandler: A closure that gets called when the framework attempts to send the
/// stream. Upon success, the handler is called with an error value of nil. Upon failure,
/// the handle is called with an error object that indicates what went wrong. This does not
/// indicate that the send has successfully completed yet. Errors might still occur during
/// transmission (and at different times for different endpoints).
/// `connectionManager(_:didReceiveTransferUpdate:from:forPayload:)`is called when delivery
/// succeeds or when an in-flight error occurs.
/// - Returns: A cancellation token used to cancel the transfer.
public func startStream(
_ stream: InputStream, to endpointIDs: [EndpointID], id: PayloadID? = nil,
completionHandler: ((Error?) -> Void)? = nil
) -> CancellationToken {
let payload: GNCStreamPayload
if let id = id {
payload = GNCStreamPayload(stream: stream, identifier: id)
} else {
payload = GNCStreamPayload(stream: stream)
}
return send(payload, to: endpointIDs, completionHandler: completionHandler)
}
/// Sends the contents of a URL to nearby endpoints.
///
/// This method is asynchronous (nonblocking).
///
/// On the recipient device, the connection manager calls its delegates
/// `connectionManager(_:didStartReceivingResourceWithID:from:at:withName:cancellationToken:)`
/// method as soon as it begins receiving the resource.
///
/// On both the local and recipient device, the connection manager instance calls its delegates
/// `connectionManager(_:didReceiveTransferUpdate:from:forPayload:)` method when delivery
/// succeeds, has a progress update or when an in-flight error occurs.
///
/// - Parameters:
/// - resourceURL: A file URL.
/// - resourceName: A name for the resource.
/// - endpointIDs: An array of endpoint IDs representing the endpoints that should receive this
/// resource.
/// - id: A unique ID for the payload. This ID is provided to the remote endpoint. A random ID
/// will be generated if none is set.
/// - completionHandler: A closure that gets called when the framework attempts to send the
/// resource. Upon success, the handler is called with an error value of nil. Upon failure,
/// the handle is called with an error object that indicates what went wrong. This does not
/// indicate that the send has successfully completed yet. Errors might still occur during
/// transmission (and at different times for different endpoints).
/// `connectionManager(_:didReceiveTransferUpdate:from:forPayload:)`is called when delivery
/// succeeds or when an in-flight error occurs.
/// - Returns: A cancellation token used to cancel the transfer.
public func sendResource(
at resourceURL: URL, withName resourceName: String, to endpointIDs: [EndpointID],
id: PayloadID? = nil, completionHandler: ((Error?) -> Void)? = nil
) -> CancellationToken {
let remotePath = URL(fileURLWithPath: resourceName)
let parentFolder = remotePath.deletingLastPathComponent().relativePath
let fileName = remotePath.lastPathComponent
let payload: GNCFilePayload
if let id = id {
payload = GNCFilePayload(
fileURL: resourceURL, parentFolder: parentFolder, fileName: fileName, identifier: id)
} else {
payload = GNCFilePayload(fileURL: resourceURL, parentFolder: parentFolder, fileName: fileName)
}
return send(payload, to: endpointIDs, completionHandler: completionHandler)
}
private func send(
_ payload: GNCPayload, to endpointIDs: [EndpointID],
completionHandler: ((Error?) -> Void)? = nil
) -> CancellationToken {
for endpointID in endpointIDs {
if transfers[payload.identifier] == nil {
transfers[payload.identifier] = [:]
}
transfers[payload.identifier]?[endpointID] = Progress()
}
GNCCoreAdapter.shared.send(payload, toEndpoints: endpointIDs) { _ in
// TODO(b/257824412): Handle errors from completion handler.
completionHandler?(nil)
}
return CancellationToken(payload.identifier)
}
/// Disconnect from a remote endpoint.
///
/// - Parameter endpointID: The ID of the remote endpoint.
public func disconnect(from endpointID: EndpointID) {
// TODO(b/257824412): Handle errors from completion handler.
GNCCoreAdapter.shared.disconnect(fromEndpoint: endpointID)
}
}
extension ConnectionManager: InternalPayloadDelegate {
func receivedPayload(_ payload: GNCPayload, fromEndpoint endpointID: String) {
queue.async { [weak self] in
guard let self = self else { return }
if self.transfers[payload.identifier] == nil {
self.transfers[payload.identifier] = [:]
}
self.transfers[payload.identifier]?[endpointID] = Progress()
let payloadID = payload.identifier
let cancellationToken = CancellationToken(payload.identifier)
if let bytesPayload = payload as? GNCBytesPayload {
self.delegate?.connectionManager(
self, didReceive: bytesPayload.data, withID: payloadID, from: endpointID)
}
if let streamPayload = payload as? GNCStreamPayload {
self.delegate?.connectionManager(
self, didReceive: streamPayload.stream, withID: payloadID, from: endpointID,
cancellationToken: cancellationToken)
}
if let filePayload = payload as? GNCFilePayload {
let name = (filePayload.parentFolder as NSString).appendingPathComponent(
filePayload.fileName)
self.delegate?.connectionManager(
self, didStartReceivingResourceWithID: payloadID, from: endpointID,
at: filePayload.fileURL, withName: name, cancellationToken: cancellationToken)
}
}
}
func receivedProgressUpdate(
forPayload payloadID: Int64, with status: GNCPayloadStatus, fromEndpoint endpointID: String,
bytesTransfered: Int64, totalBytes: Int64
) {
queue.async { [weak self] in
guard let self = self, let progress = self.transfers[payloadID]?[endpointID] else { return }
progress.totalUnitCount = totalBytes
progress.completedUnitCount = bytesTransfered
switch status {
case .success:
self.transfers[payloadID]?.removeValue(forKey: endpointID)
self.delegate?.connectionManager(
self, didReceiveTransferUpdate: .success, from: endpointID, forPayload: payloadID)
case .failure:
self.transfers[payloadID]?.removeValue(forKey: endpointID)
// TODO(b/257824412): Handle errors from completion handler.
break
case .canceled:
self.transfers[payloadID]?.removeValue(forKey: endpointID)
self.delegate?.connectionManager(
self, didReceiveTransferUpdate: .canceled, from: endpointID, forPayload: payloadID)
case .inProgress:
self.delegate?.connectionManager(
self, didReceiveTransferUpdate: .progress(progress), from: endpointID,
forPayload: payloadID)
}
if self.transfers[payloadID]?.isEmpty ?? false {
self.transfers.removeValue(forKey: payloadID)
}
}
}
}
/// The `ConnectionManagerDelegate` protocol defines methods that a `ConnectionManager` objects
/// delegate can implement to handle connection-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 ConnectionManagerDelegate: AnyObject {
/// Called to validate the verification code generated for the pair of endpoints.
///
/// Your app should confirm whether you connected to the correct endpoint. Typically this involves
/// showing the verification code on both devices and having the users manually compare and
/// confirm; however, this is only required if you desire a secure connection between the devices.
/// Upon confirmation, your app should call the provided `verificationHandler` block, passing
/// either `true` (to trust the nearby endpoint) or `false` (to reject it).
///
/// Important: The Nearby Connections framework makes no attempt to validate the verification
/// code in any way. If your delegate does not implement this method, all verification codes
/// are accepted automatically.
///
/// - Parameters:
/// - connectionManager: The connection manager object through which the verification code was
/// received.
/// - verificationCode: An identical token given to both endpoints.
/// - endpointID: The endpoint ID of the nearby endpoint that needs verification.
/// - verificationHandler: Your app should call this handler with a value of `true` if the
/// nearby endpoint should be trusted, or `false` otherwise.
func connectionManager(
_ connectionManager: ConnectionManager, didReceive verificationCode: String,
from endpointID: EndpointID, verificationHandler: @escaping (Bool) -> Void)
/// Indicates that a `Data` object has been received from a nearby endpoint.
///
/// - Parameters:
/// - connectionManager: The connection manager object through which the data was received.
/// - data: An object containing the received data.
/// - payloadID: The ID of the payload, as provided by the sender.
/// - endpointID: The endpoint ID of the sender.
func connectionManager(
_ connectionManager: ConnectionManager, didReceive data: Data, withID payloadID: PayloadID,
from endpointID: EndpointID)
/// Called when a nearby endpoint opens a byte stream connection to the local endpoint.
///
/// - Parameters:
/// - connectionManager: The connection manager object through which the byte stream was
/// received.
/// - stream: An `InputStream` object that represents the local endpoint for the byte stream.
/// - payloadID: The ID of the payload, as provided by the sender.
/// - endpointID: The endpoint ID of the originator of the stream.
/// - token: Token used to cancel the stream.
func connectionManager(
_ connectionManager: ConnectionManager, didReceive stream: InputStream,
withID payloadID: PayloadID,
from endpointID: EndpointID, cancellationToken token: CancellationToken)
/// Indicates that the local endpoint began receiving a resource from a nearby endpoint.
///
/// - Parameters:
/// - connectionManager: The connection manager object that started receiving the resource.
/// - payloadID: The ID of the payload, as provided by the sender.
/// - endpointID: The endpoint ID of the sender.
/// - localURL: Provides the location of a file containing the data being received.
/// - name: The name of the resource, as provided by the sender.
/// - token: Token used to cancel the resource transfer.
func connectionManager(
_ connectionManager: ConnectionManager, didStartReceivingResourceWithID payloadID: PayloadID,
from endpointID: EndpointID, at localURL: URL, withName name: String,
cancellationToken token: CancellationToken)
/// Called when a progress update is available for an incoming or outgoing transfer.
///
/// - Parameters:
/// - connectionManager: The connection manager object that received the update.
/// - update: A success, failure, cancellation or progress update.
/// - endpointID: The remote endpoint ID.
/// - payloadID: The ID of the payload, as provided by the sender.
func connectionManager(
_ connectionManager: ConnectionManager, didReceiveTransferUpdate update: TransferUpdate,
from endpointID: EndpointID, forPayload payloadID: PayloadID)
/// Called when the status of the connection to a nearby endpoint changes.
///
/// - Parameters:
/// - connectionManager: The connection manager object that manages the connection whose state
/// changed.
/// - state: The new state of the connection.
/// - endpointID: The ID of the nearby endpoint whose connection status has changed.
func connectionManager(
_ connectionManager: ConnectionManager, didChangeTo state: ConnectionState,
for endpointID: EndpointID
)
}
extension ConnectionManagerDelegate {
public func connectionManager(
_ connectionManager: ConnectionManager, didReceive verificationCode: String,
from endpointID: EndpointID, verificationHandler: @escaping (Bool) -> Void
) {
verificationHandler(true)
}
}