Add macOS support (#23)

* Add macOS support

* Update README

* Update to v0.2.0

* Document breaking changes

* Delete widget tests
This commit is contained in:
Ben Hagen
2025-05-13 16:24:58 +02:00
committed by GitHub
parent c391cc5f26
commit abe519dae7
103 changed files with 3444 additions and 169 deletions
+38
View File
@@ -0,0 +1,38 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/ephemeral/
/Flutter/flutter_export_environment.sh
View File
+37
View File
@@ -0,0 +1,37 @@
//
// NearbyCommand.swift
// nearby_service
//
// Created by Kseniia Nikitina on 4/2/24.
//
import Foundation
import MultipeerConnectivity
class NearbyStartCommand {
init(id: String, senderName: String, filesCount: Int) {
self.id = id
self.senderName = senderName
self.filesCount = filesCount
}
static func fromUserInfo(userInfo: NearbyUserInfo)-> NearbyStartCommand? {
if let name = userInfo.dictionary["name"] as? String,
let filesCount = userInfo.dictionary["filesCount"] as? Int,
let id = userInfo.dictionary["id"] as? String
{
return NearbyStartCommand(id: id, senderName: name, filesCount: filesCount)
}
return nil
}
func toDictionary() -> [String: Any] {
return ["name": senderName, "filesCount": filesCount, "id": id]
}
let id: String
let senderName: String
let filesCount: Int
}
+86
View File
@@ -0,0 +1,86 @@
import Foundation
import MultipeerConnectivity
class NearbyDevice : NSObject {
var name: String
var peerID: MCPeerID
var deviceType: String?
var os: String?
var osVersion: String?
var session: NearbySession?
init(
peerID: MCPeerID,
name:String,
deviceType:String? = nil,
os: String? = nil,
osVersion: String? = nil
) {
self.name=name
self.peerID = peerID
self.deviceType=deviceType
self.os=os
self.osVersion=osVersion
}
func createSession(for peerID: MCPeerID) -> NearbySession {
self.session = NearbySession.create(peerID: peerID)
return session!
}
func deleteSession() {
self.session = nil
}
}
extension NearbyDevice {
static func fromDictionary(for dictionary: [String: String]?, with peerID: MCPeerID) -> NearbyDevice {
let device = NearbyDevice(
peerID: peerID,
name:(dictionary?["displayName"] as String?) ?? peerID.displayName,
deviceType: dictionary?["deviceType"] as String?,
os: dictionary?["os"] as String?,
osVersion: dictionary?["osVersion"] as String?
)
return device
}
func toDictionary() -> [String: String] {
var deviceDict: [String: String] = [
"displayName": self.name,
"id": self.peerID.displayName,
]
if let os = self.os {
deviceDict["os"] = os
}
if let deviceType = self.deviceType {
deviceDict["deviceType"] = deviceType
}
if let osVersion = self.osVersion {
deviceDict["osVersion"] = osVersion
}
if let session = self.session {
deviceDict["state"] = String(session.state.rawValue)
} else {
deviceDict["state"] = String(0)
}
return deviceDict
}
func toDartFormat() -> String? {
var result: String?
do {
let jsonData = try JSONSerialization.data(withJSONObject: toDictionary())
if let jsonString = String(data: jsonData, encoding: .utf8) {
result = jsonString
}
} catch let error {
Logger.error(message: error.localizedDescription)
}
return result
}
}
+60
View File
@@ -0,0 +1,60 @@
//
// NearbyMessage.swift
// nearby_service
//
// Created by Kseniia Nikitina on 4/2/24.
//
import Foundation
import MultipeerConnectivity
class NearbyMessage {
init(content: NearbyMessageContent, senderName: String, senderPeerID: MCPeerID) {
self.content = content
self.senderName = senderName
self.senderPeerID = senderPeerID
}
let content: NearbyMessageContent
let senderPeerID: MCPeerID
let senderName: String
static func fromUserInfo(userInfo: NearbyUserInfo)-> NearbyMessage? {
if let jsonContent = userInfo.dictionary["content"] as? [String : Any],
let content = NearbyMessageContent.typedFromJson(json: jsonContent),
let name = userInfo.dictionary["name"] as? String {
return NearbyMessage(
content: content,
senderName: name,
senderPeerID: userInfo.peerID
)
}
return nil
}
func toDictionary() -> [String: Any] {
return [
"name": senderName,
"content": content.toJson()
]
}
func toDartFormat() -> String? {
do {
let object = [
"sender": ["id": senderPeerID.displayName, "displayName": senderName],
"content": content.toJson()
]
let jsonData = try JSONSerialization.data(withJSONObject: object)
if let jsonString = String(data: jsonData, encoding: .utf8) {
return jsonString
}
} catch let error {
Logger.error(message: error.localizedDescription)
}
return nil
}
}
@@ -0,0 +1,163 @@
//
// NearbyMessage.swift
// nearby_service
//
// Created by Kseniia Nikitina on 4/2/24.
//
import Foundation
import MultipeerConnectivity
class NearbyMessageContent {
init(id: String, type: MessageContentType) {
self.type = type
self.id = id
}
let type: MessageContentType
static func typedFromJson(json: [String: Any]) -> NearbyMessageContent? {
if let typeString: String = json["type"] as? String {
let type = MessageContentType.fromString(value: typeString)
if (type == MessageContentType.textRequest) {
return NearbyMessageTextRequest.fromJson(json: json)
} else if (type == MessageContentType.textResponse) {
return NearbyMessageTextResponse.fromJson(json: json)
} else if (type == MessageContentType.filesRequest) {
return NearbyMessageFilesRequest.fromJson(json: json)
} else if (type == MessageContentType.filesResponse) {
return NearbyMessageFilesResponse.fromJson(json: json)
}
}
return nil;
}
static func fromJsonRaw(type: MessageContentType, json: [String: Any]) -> NearbyMessageContent? {
if let id: String = json["id"] as? String {
return NearbyMessageContent(id: id, type: type)
}
return nil;
}
func toJson() -> [String : Any] {
return ["type": type.name, "id": id]
}
let id: String
}
class NearbyMessageTextRequest : NearbyMessageContent {
init(id: String, value: String) {
self.value = value
super.init(id: id, type: MessageContentType.textRequest)
}
static func fromJson(json:[String: Any]) -> NearbyMessageTextRequest? {
if let value: String = json["value"] as? String,
let content = NearbyMessageContent.fromJsonRaw(type: MessageContentType.textRequest, json:json) {
return NearbyMessageTextRequest(id: content.id, value: value)
}
return nil;
}
override func toJson() -> [String : Any] {
return ["value": value].merging( super.toJson()) { (current, _) in current}
}
let value: String
}
class NearbyMessageTextResponse :NearbyMessageContent {
init(id: String) {
super.init(id: id, type: MessageContentType.textResponse)
}
static func fromJson( json: [String: Any]) -> NearbyMessageTextResponse? {
let content = NearbyMessageContent.fromJsonRaw(type: MessageContentType.textResponse, json: json)
if let requireContent = content {
return NearbyMessageTextResponse(id: requireContent.id)
}
return nil
}
}
class NearbyMessageFilesRequest : NearbyMessageContent {
init(files: Array<String>, id: String) {
self.files = files
super.init(id: id, type: MessageContentType.filesRequest)
}
static func fromJson(json: [String: Any]) -> NearbyMessageFilesRequest? {
let content = NearbyMessageContent.fromJsonRaw(type: MessageContentType.filesRequest, json: json)
if let filesObjects: Array = json["files"] as? Array<Dictionary<String, AnyObject>> {
let files : [String]? = filesObjects.map({ $0["path"] as? String }).compactMap({$0})
if let requireContent = content {
if let requireFiles = files {
return NearbyMessageFilesRequest(files: requireFiles, id: requireContent.id)
}
}
}
return nil;
}
let files: Array<String>
override func toJson() -> [String : Any] {
return [
"files": files.map{["path": $0]},
].merging( super.toJson()) { (current, _) in current}
}
}
class NearbyMessageFilesResponse : NearbyMessageContent {
init(id: String, response: Bool) {
self.response = response
super.init(id: id, type: MessageContentType.filesResponse)
}
static func fromJson( json: [String: Any]) -> NearbyMessageFilesResponse? {
let message = NearbyMessageContent.fromJsonRaw(type: MessageContentType.filesResponse, json: json)
if let requireMessage = message,
let response = json["isAccepted"] as? Bool {
return NearbyMessageFilesResponse(id: requireMessage.id, response: response)
}
return nil
}
override func toJson() -> [String : Any] {
return [
"isAccepted": response
].merging( super.toJson()) { (current, _) in current}
}
let response: Bool
}
enum MessageContentType {
case textRequest
case textResponse
case filesRequest
case filesResponse
static func fromString(value: String) -> MessageContentType {
if (value == textRequest.name) {
return textRequest
} else if (value == textResponse.name) {
return textResponse
} else if (value == filesRequest.name) {
return filesRequest
} else if (value == filesResponse.name) {
return filesResponse
} else {
return textRequest
}
}
var name : String {
switch self {
case .textRequest: return "textRequest"
case .textResponse: return "textResponse"
case .filesRequest: return "filesRequest"
case .filesResponse: return "filesResponse"
}
}
}
+67
View File
@@ -0,0 +1,67 @@
//
// NearbySession.swift
// nearby_service
//
// Created by Kseniia Nikitina on 16/1/24.
//
import Foundation
import MultipeerConnectivity
class NearbySession: NSObject {
var session: MCSession!
var state: MCSessionState = MCSessionState.notConnected
private init(peerID: MCPeerID) {
self.session = MCSession(peer: peerID)
}
static func create(peerID: MCPeerID) -> NearbySession {
let instance = NearbySession(peerID: peerID)
instance.session.delegate = instance
return instance
}
}
extension NearbySession: MCSessionDelegate {
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
self.state = state
}
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
NotificationCenter.default.post(
name: ON_MESSAGE_RECEIVED,
object: nil,
userInfo: NearbyUserInfo.message(peerID: peerID, data: data)?.toDictionary()
)
}
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {}
func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
}
func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
guard let localURL = localURL else { return }
var destinationURL = localURL.deletingLastPathComponent().appendingPathComponent("\(resourceName)")
if FileManager.default.fileExists(atPath: destinationURL.path) {
destinationURL = localURL.deletingLastPathComponent().appendingPathComponent("New_\(resourceName)")
}
do {
try FileManager.default.moveItem(at: localURL, to: destinationURL)
} catch {
Logger.error(message: "Error moving file: \(error)")
}
NotificationCenter.default.post(
name: ON_RESOURCE_RECEIVED,
object: nil,
userInfo: NearbyUserInfo.resource(peerID: peerID, url: destinationURL)?.toDictionary()
)
}
}
@@ -0,0 +1,58 @@
//
// NearbyUserInfo.swift
// nearby_service
//
// Created by Kseniia Nikitina on 4/2/24.
//
import Foundation
import MultipeerConnectivity
class NearbyUserInfo {
init(peerID: MCPeerID, dictionary: [String: Any]) {
self.peerID = peerID
self.dictionary = dictionary
}
static func resource(peerID: MCPeerID, url: URL?) -> NearbyUserInfo? {
var dictionary: [String: Any] = [:]
if let requireUrl = url {
dictionary["url"] = requireUrl
}
return NearbyUserInfo(
peerID: peerID,
dictionary: dictionary
)
}
static func message(peerID: MCPeerID, data: Data) -> NearbyUserInfo? {
do {
if let dict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
return NearbyUserInfo(
peerID: peerID,
dictionary: dict
)
}
} catch let error {
Logger.error(message: error.localizedDescription)
}
return nil
}
static func fromDictionary(userInfo: [AnyHashable : Any]?) -> NearbyUserInfo? {
if let dictionary = userInfo?["dictionary"] as? [String: Any],
let peerID = userInfo?["peerID"] as? MCPeerID {
return NearbyUserInfo(peerID: peerID, dictionary:dictionary)
}
return nil
}
func toDictionary() -> [AnyHashable : Any]? {
return ["peerID": peerID, "dictionary": dictionary]
}
let peerID: MCPeerID
let dictionary: [String: Any]
}
+232
View File
@@ -0,0 +1,232 @@
import Foundation
#if os(iOS)
import Flutter
import UIKit
#elseif os(macOS)
import FlutterMacOS
import AppKit
#endif
import MultipeerConnectivity
class NearbyManager: NSObject {
var device: NearbyDevice!
var advertiser: MCNearbyServiceAdvertiser!
var browser: MCNearbyServiceBrowser!
var invitationHandlers: [String: ((Bool, MCSession?) -> Void)] = [:]
func initialize(for deviceName: String? = nil, result: @escaping FlutterResult) {
self.device = MyDeviceDataGenerator.generate(name: deviceName)
self.advertiser = MCNearbyServiceAdvertiser(
peer: self.device.peerID,
discoveryInfo: self.device.toDictionary(),
serviceType: SERVICE_TYPE
)
self.advertiser.delegate = self
self.browser = MCNearbyServiceBrowser(peer: self.device.peerID, serviceType: SERVICE_TYPE)
self.browser.delegate = self
result(true)
}
func getSavedDeviceName(result: @escaping FlutterResult) {
result(Archiver.getName())
}
func getCurrentDevice(result: @escaping FlutterResult) {
if (!checkInitialization(result: result)) { return }
result(device.toDartFormat())
}
func openServicesSettings(result: @escaping FlutterResult) {
#if os(iOS)
if let url = URL(string:UIApplication.openSettingsURLString) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
#elseif os(macOS)
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.sharing") {
NSWorkspace.shared.open(url)
}
#endif
result(true)
}
func startAdvertising(result: @escaping FlutterResult) {
if (!checkInitialization(result: result)) { return }
self.advertiser.startAdvertisingPeer()
result(true)
}
func startBrowsing(result: @escaping FlutterResult) {
if (!checkInitialization(result: result)) { return }
self.browser.startBrowsingForPeers()
result(true)
}
func stopAdvertising(result: @escaping FlutterResult) {
if (!checkInitialization(result: result)) { return }
self.advertiser.stopAdvertisingPeer()
NearbyDevicesStore.instance.clear()
result(true)
}
func stopBrowsing(result: @escaping FlutterResult) {
if (!checkInitialization(result: result)) { return }
self.browser.stopBrowsingForPeers()
NearbyDevicesStore.instance.clear()
result(true)
}
func getPeers(result: @escaping FlutterResult) {
if (!checkInitialization(result: result)) { return }
result(NearbyDevicesStore.instance.toDartFormat())
}
func invite(for deviceId: String, result: @escaping FlutterResult) {
if (!checkInitialization(result: result)) { return }
do {
let device = NearbyDevicesStore.instance.find(for: deviceId)
if let requireDevice = device {
let nearbySession = requireDevice.createSession(for: self.device.peerID)
self.browser.invitePeer(
requireDevice.peerID,
to: nearbySession.session,
withContext: try JSONSerialization.data(withJSONObject:["displayName": self.device.name]),
timeout: 0
)
result(true)
}
} catch let error {
Logger.error(message: error.localizedDescription)
result(false)
}
}
func acceptInvite(for deviceId: String, result: @escaping FlutterResult) {
if (!checkInitialization(result: result)) { return }
let device = NearbyDevicesStore.instance.find(for: deviceId)
if let requireDevice = device {
let nearbySession = requireDevice.createSession(for: self.device.peerID)
self.invitationHandlers[deviceId]?(true, nearbySession.session)
result(true)
}
}
func disconnect(for deviceId: String, result: @escaping FlutterResult) {
if (!checkInitialization(result: result)) { return }
let device = NearbyDevicesStore.instance.find(for: deviceId)
device?.deleteSession()
result(true)
}
func send(for content: NearbyMessageContent, with receiverId: String, result: @escaping FlutterResult) {
if (!checkInitialization(result: result)) { return }
let device = NearbyDevicesStore.instance.find(for: receiverId)
do {
if let requireDevice = device {
let message = NearbyMessage(content: content, senderName: self.device.name, senderPeerID: self.device.peerID)
if (content is NearbyMessageFilesRequest) {
NearbyRequestsStore.instance.add(request: message.content as! NearbyMessageFilesRequest)
}
try requireDevice.session?.session?.send(
try JSONSerialization.data(withJSONObject: message.toDictionary()),
toPeers: [requireDevice.peerID],
with: MCSessionSendDataMode.reliable
)
}
} catch let error {
Logger.error(message: error.localizedDescription)
}
result(true)
}
func sendFiles(id: String, paths: [String], with receiverId: String) {
do {
let device = NearbyDevicesStore.instance.find(for: receiverId)
if let requireDevice = device {
let command = NearbyStartCommand(
id: id,
senderName: self.device.name,
filesCount: paths.count
).toDictionary()
try requireDevice.session?.session?.send(
try JSONSerialization.data(withJSONObject: command),
toPeers: [requireDevice.peerID],
with: MCSessionSendDataMode.reliable
)
for path in paths {
let url = URL(fileURLWithPath: path)
if FileManager.default.fileExists(atPath: url.path) {
requireDevice.session?.session?.sendResource(
at: url,
withName: url.lastPathComponent,
toPeer: requireDevice.peerID
)
} else {
Logger.error(message: "File does not exist: " + path)
}
}
}
} catch let error {
Logger.error(message: error.localizedDescription)
}
}
func checkInitialization(result: @escaping FlutterResult) -> Bool {
guard let _ = self.device,
let _ = self.advertiser,
let _ = self.browser else {
Logger.error(message: "NearbyManager is not initialized. Please call 'initialize()' first")
result(ERROR_NO_INITIALIZATION)
return false
}
return true
}
}
extension NearbyManager: MCNearbyServiceAdvertiserDelegate {
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void
) {
var dict: Dictionary<String, String>?
do {
dict = try JSONSerialization.jsonObject(with: context ?? Data()) as? Dictionary<String, String>
} catch let error {
Logger.error(message: error.localizedDescription)
}
_ = NearbyDevicesStore.instance.add(for: peerID, discoveryInfo: dict)
self.invitationHandlers[peerID.displayName] = invitationHandler
}
}
extension NearbyManager: MCNearbyServiceBrowserDelegate {
func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
_ = NearbyDevicesStore.instance.add(for: peerID, discoveryInfo: info)
}
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
let device = NearbyDevicesStore.instance.find(for: peerID.displayName)
device?.deleteSession()
NearbyDevicesStore.instance.remove(for: peerID)
}
}
@@ -0,0 +1,63 @@
//
// NearbyServicePluginOnReceived.swift
// nearby_service
//
// Created by Kseniia Nikitina on 4/2/24.
//
import Foundation
#if os(iOS)
import Flutter
#elseif os(macOS)
import FlutterMacOS
#endif
extension NearbyServicePlugin {
@objc func onMessageReceived(notification: Notification) {
DispatchQueue.main.async {
if let userInfo = NearbyUserInfo.fromDictionary(userInfo: notification.userInfo) {
print(userInfo)
if let message = NearbyMessage.fromUserInfo(userInfo: userInfo) {
print(message)
if message.content is NearbyMessageFilesResponse {
print(message.content)
let response = message.content as! NearbyMessageFilesResponse
let cachedRequest = NearbyRequestsStore.instance.find(for: response.id)
if (response.response && cachedRequest != nil) {
print("send")
self.manager.sendFiles(
id: cachedRequest!.id,
paths: cachedRequest!.files,
with: message.senderPeerID.displayName
)
NearbyRequestsStore.instance.remove(for: cachedRequest!.id)
}
}
self.channel.invokeMethod(DART_COMMAND_MESSAGE_RECEIVED, arguments: message.toDartFormat())
} else if let command = NearbyStartCommand.fromUserInfo(userInfo: userInfo) {
NearbyFilesStore.instance.startReceiving(command: command)
}
}
}
}
@objc func onResourceReceived(notification: Notification) {
DispatchQueue.main.async {
if let userInfo = NearbyUserInfo.fromDictionary(userInfo: notification.userInfo) {
if let url = userInfo.dictionary["url"] as? URL {
NearbyFilesStore.instance.add(url: url)
if (NearbyFilesStore.instance.checkIsFull()) {
self.channel.invokeMethod(DART_COMMAND_RESOURCES_RECEIVED, arguments: NearbyFilesStore.instance.toDartFormat(peerID: userInfo.peerID))
NearbyFilesStore.instance.clear()
}
}
}
}
}
}
+156
View File
@@ -0,0 +1,156 @@
#if os(iOS)
import Flutter
import UIKit
#elseif os(macOS)
import FlutterMacOS
import AppKit
import Foundation
#endif
import MultipeerConnectivity
public class NearbyServicePlugin: NSObject, FlutterPlugin{
let manager: NearbyManager;
let channel: FlutterMethodChannel
init(manager: NearbyManager, channel: FlutterMethodChannel) {
self.manager = manager
self.channel = channel
}
public static func register(with registrar: FlutterPluginRegistrar) {
// Workaround for https://github.com/flutter/flutter/issues/118103.
#if os(iOS)
let messenger = registrar.messenger()
#else
let messenger = registrar.messenger
#endif
let channel = FlutterMethodChannel(
name: "nearby_service",
binaryMessenger: messenger
)
let nearbyPeersChannel = FlutterEventChannel(
name: "nearby_service_peers",
binaryMessenger: messenger
)
let connectedDeviceChannel = FlutterEventChannel(
name: "nearby_service_connected_device",
binaryMessenger: messenger
)
let nearbyPeersStreamHandler = NearbyPeersStreamHandler()
nearbyPeersChannel.setStreamHandler(nearbyPeersStreamHandler)
let connectedDeviceStreamHandler = ConnectedDeviceStreamHandler()
connectedDeviceChannel.setStreamHandler(connectedDeviceStreamHandler)
let manager = NearbyManager()
let instance = NearbyServicePlugin(manager: manager, channel: channel)
NotificationCenter.default.addObserver(
instance,
selector: #selector(onMessageReceived),
name: ON_MESSAGE_RECEIVED,
object: nil
)
NotificationCenter.default.addObserver(
instance,
selector: #selector(onResourceReceived),
name: ON_RESOURCE_RECEIVED,
object: nil
)
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func detachFromEngine(for registrar: FlutterPluginRegistrar) {
NearbyDevicesStore.instance.clear()
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
#if os(iOS)
result("iOS " + UIDevice.current.systemVersion)
#elseif os(macOS)
result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString)
#endif
case "getPlatformModel":
#if os(iOS)
result(UIDevice.current.name)
#elseif os(macOS)
result(Host.current().localizedName ?? "Mac")
#endif
case "initialize":
manager.initialize(for: getArgument(for: "deviceName", call: call), result: result)
case "getSavedDeviceName":
manager.getSavedDeviceName(result: result)
case "getCurrentDevice":
manager.getCurrentDevice(result: result)
case "openServicesSettings":
manager.openServicesSettings(result: result)
case "startAdvertising":
manager.startAdvertising(result: result)
case "startBrowsing":
manager.startBrowsing(result: result)
case "stopAdvertising":
manager.stopAdvertising(result: result)
case "stopBrowsing":
manager.stopBrowsing(result: result)
case "getPeers":
manager.getPeers(result: result)
case "invite":
if let deviceId: String = getArgument(for: "deviceId", call: call) {
manager.invite(for: deviceId, result: result)
} else {
result(false)
}
case "acceptInvite":
if let deviceId : String = getArgument(for: "deviceId", call: call) {
manager.acceptInvite(for: deviceId, result: result)
} else {
result(false)
}
case "disconnect":
if let deviceId: String = getArgument(for: "deviceId", call: call) {
manager.disconnect(for: deviceId, result: result)
} else {
result(false)
}
case "send":
if let contentJson: Dictionary<String, AnyObject> = getArgument(for: "content", call: call),
let content: NearbyMessageContent = NearbyMessageContent.typedFromJson(json: contentJson) {
if let receiver : Dictionary<String, AnyObject> = getArgument(for: "receiver", call: call),
let receiverId : String = receiver["id"] as? String
{
manager.send(for: content, with: receiverId, result: result)
} else {
result(false)
}
} else {
result(false)
}
default:
result(FlutterMethodNotImplemented)
}
}
func getArgument<T>(for name: String, call: FlutterMethodCall) -> T? {
guard let data = call.arguments as? Dictionary<String, AnyObject> else {
return nil
}
guard let argument: T = data[name] as? T else {
return nil
}
return argument
}
}
@@ -0,0 +1,59 @@
//
// NearbyDevicesStore.swift
// nearby_service
//
// Created by Kseniia Nikitina on 16/1/24.
//
import Foundation
import MultipeerConnectivity
class NearbyDevicesStore : NSObject {
static let instance = NearbyDevicesStore()
private var devices : [NearbyDevice] = []
func getDevices() -> [NearbyDevice] {
return devices
}
func find(for deviceId: String) -> NearbyDevice? {
return devices.first { device in
return device.peerID.displayName == deviceId
}
}
func add(for peerID: MCPeerID, discoveryInfo: [String: String]? = nil) -> NearbyDevice? {
devices = devices.filter{$0.peerID.displayName != peerID.displayName}
let device = NearbyDevice.fromDictionary(for: discoveryInfo, with: peerID)
self.devices.append(device)
return device
}
func remove(for peerID: MCPeerID) {
self.devices = devices.filter{$0.peerID.displayName != peerID.displayName}
}
func clear() {
self.devices = []
}
func toDartFormat() -> String {
let devicesObject = devices.map { device in
return device.toDictionary()
}
do {
let jsonData = try JSONSerialization.data(withJSONObject: devicesObject)
if let jsonString = String(data: jsonData, encoding: .utf8) {
return jsonString
}
} catch {
return "[]"
}
return "[]"
}
}
@@ -0,0 +1,64 @@
//
// NearbyFilesStore.swift
// nearby_service
//
// Created by Kseniia Nikitina on 3/2/24.
//
import Foundation
import MultipeerConnectivity
class NearbyFilesStore {
static let instance = NearbyFilesStore()
private var paths : [String] = []
private var senderName: String? = nil
private var maxCount: Int = 0
private var count: Int = 0
private var id: String? = nil
func startReceiving(command: NearbyStartCommand) {
self.paths.removeAll()
self.id = command.id
self.senderName = command.senderName
self.maxCount = command.filesCount
self.count = 0
}
func add(url: URL) {
paths.append(url.path)
self.count = self.count + 1
}
func checkIsFull() -> Bool {
return maxCount <= count
}
func clear() {
self.id = nil
self.paths.removeAll()
self.senderName = nil
self.maxCount = 0
self.count = 0
}
func toDartFormat(peerID: MCPeerID) -> String? {
if (senderName != nil && id != nil) {
let object = [
"id": id!,
"files": paths.map { ["path": $0]},
"sender": ["id": peerID.displayName, "displayName": senderName!]
] as [String : Any]
do {
let jsonData = try JSONSerialization.data(withJSONObject: object)
if let jsonString = String(data: jsonData, encoding: .utf8) {
return jsonString
}
} catch {
return nil
}
return nil
}
return nil
}
}
@@ -0,0 +1,29 @@
//
// NearbyRequestsStore.swift
// nearby_service
//
// Created by Kseniia Nikitina on 5/2/24.
//
import Foundation
class NearbyRequestsStore {
static let instance = NearbyRequestsStore()
private var requests : [NearbyMessageFilesRequest] = []
func add(request: NearbyMessageFilesRequest) {
requests.append(request)
}
func find(for id: String) -> NearbyMessageFilesRequest? {
return requests.first { request in
return request.id == id
}
}
func remove(for id: String) {
self.requests = requests.filter{$0.id != id}
}
}
@@ -0,0 +1,43 @@
#if os(iOS)
import Flutter
#elseif os(macOS)
import FlutterMacOS
#endif
import MultipeerConnectivity
class ConnectedDeviceStreamHandler: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
private var timer : Timer?
func onListen(withArguments arguments: Any?, eventSink: @escaping FlutterEventSink) -> FlutterError? {
let deviceId = arguments as! String
self.eventSink = eventSink
startSendingUpdates(deviceId: deviceId)
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
stopSendingUpdates()
eventSink = nil
return nil
}
func startSendingUpdates(deviceId:String) {
self.timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
var result: String?
if let device = NearbyDevicesStore.instance.find(for: deviceId),
let session = device.session {
if (session.state == MCSessionState.connected) {
result = device.toDartFormat()
}
}
self.eventSink?(result)
}
}
func stopSendingUpdates() {
self.timer?.invalidate()
self.timer = nil
}
}
@@ -0,0 +1,36 @@
#if os(iOS)
import Flutter
#elseif os(macOS)
import FlutterMacOS
#endif
class NearbyPeersStreamHandler: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
private var timer : Timer?
func onListen(withArguments arguments: Any?, eventSink: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = eventSink
startSendingUpdates()
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
stopSendingUpdates()
eventSink = nil
return nil
}
func startSendingUpdates() {
self.timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
let devicesList = NearbyDevicesStore.instance.toDartFormat()
self.eventSink?(devicesList)
}
}
func stopSendingUpdates() {
self.timer?.invalidate()
self.timer = nil
}
}
+56
View File
@@ -0,0 +1,56 @@
//
// ArchivedData.swift
// nearby_service
//
// Created by Kseniia Nikitina on 4/2/24.
//
import Foundation
import MultipeerConnectivity
class Archiver {
static func getPeerID() -> MCPeerID? {
guard let savedData = UserDefaults.standard.data(forKey: PEER_ID),
let unarchivedPeerID = try? NSKeyedUnarchiver.unarchivedObject(ofClass: MCPeerID.self, from: savedData)
else {
return nil
}
return unarchivedPeerID
}
static func savePeerID(for peerID: MCPeerID) {
do {
UserDefaults.standard.set(
try NSKeyedArchiver.archivedData(withRootObject: peerID, requiringSecureCoding: false),
forKey: PEER_ID
)
} catch let e {
Logger.error(message: e.localizedDescription)
}
}
static func getName() -> String? {
guard let savedData = UserDefaults.standard.data(forKey: DEVICE_NAME),
let unarchivedName = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSData.self, from: savedData)
else {
return nil
}
return String(data: unarchivedName as Data, encoding: .utf8)
}
static func saveName(for name: String) {
if let data = name.data(using: .utf8) {
do {
UserDefaults.standard.set(
try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: false),
forKey: DEVICE_NAME
)
} catch let e {
Logger.error(message: e.localizedDescription)
}
}
}
}
+20
View File
@@ -0,0 +1,20 @@
//
// Constants.swift
// nearby_service
//
// Created by Kseniia Nikitina on 4/2/24.
//
import Foundation
let SERVICE_TYPE = "mp-connection"
let PEER_ID = "PEER-ID"
let DEVICE_NAME = "DEVICE-NAME"
let ON_MESSAGE_RECEIVED = Notification.Name("NearbySessionOnMessageReceived")
let ON_RESOURCE_RECEIVED = Notification.Name("NearbySessionOnResourceReceived")
let DART_COMMAND_MESSAGE_RECEIVED = "invoke_nearby_service_message_received"
let DART_COMMAND_RESOURCES_RECEIVED = "invoke_nearby_service_resources_received"
let ERROR_NO_INITIALIZATION = "NO_INITIALIZATION"
+15
View File
@@ -0,0 +1,15 @@
//
// Logger.swift
// nearby_service
//
// Created by Kseniia Nikitina on 26/1/24.
//
import Foundation
class Logger {
static func error(message: String) {
NSLog("NearbyServicePluginError -- %@", message)
}
}
@@ -0,0 +1,80 @@
import Foundation
import MultipeerConnectivity
#if os(iOS)
import UIKit
#elseif os(macOS)
import AppKit
import Foundation
#endif
class MyDeviceDataGenerator {
static func generate(name: String?) -> NearbyDevice {
return NearbyDevice(
peerID: getPeerID(),
name: getNameArchived(or: name),
deviceType: getDeviceType(),
os: getOSName(),
osVersion: getOSVersion()
)
}
static private func getDeviceType() -> String {
#if os(iOS)
return UIDevice.current.model
#elseif os(macOS)
return "Mac"
#endif
}
static private func getOSName() -> String {
#if os(iOS)
return UIDevice.current.systemName
#elseif os(macOS)
return "macOS"
#endif
}
static private func getOSVersion() -> String {
#if os(iOS)
return UIDevice.current.systemVersion
#elseif os(macOS)
return ProcessInfo.processInfo.operatingSystemVersionString
#endif
}
static private func getPeerID() -> MCPeerID {
if let archivedPeerID = Archiver.getPeerID() {
return archivedPeerID
} else {
let deviceName: String
#if os(iOS)
deviceName = UIDevice.current.name
#elseif os(macOS)
deviceName = Host.current().localizedName ?? "Mac"
#endif
let peerID = MCPeerID(
displayName: deviceName.replacingOccurrences(of: " ", with: "_")
+ "_"
+ String(Int.random(in: 1000..<50000))
)
Archiver.savePeerID(for: peerID)
return peerID
}
}
static private func getNameArchived(or name: String?) -> String {
let archivedName = Archiver.getName()
if let newName = name {
if (archivedName != newName) {
Archiver.saveName(for: newName)
}
return newName
}
#if os(iOS)
return archivedName ?? UIDevice.current.name
#elseif os(macOS)
return archivedName ?? (Host.current().localizedName ?? "Mac")
#endif
}
}
+25
View File
@@ -0,0 +1,25 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint nearby_service.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'nearby_service'
s.version = '0.0.1'
s.summary = 'A new Flutter plugin project.'
s.description = <<-DESC
A new Flutter plugin project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.ios.dependency 'Flutter'
s.osx.dependency 'FlutterMacOS'
s.ios.deployment_target = '12.0'
s.osx.deployment_target = '10.14'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end