From 66a79eb4ba686f158eb45da6fab162c6c90bd95b Mon Sep 17 00:00:00 2001 From: ksenia312 Date: Sun, 4 Feb 2024 16:23:48 +0100 Subject: [PATCH] feat(ios): files sending --- example/lib/main.dart | 7 +- example/pubspec.yaml | 1 + ios/Classes/Models/NearbyCommand.swift | 35 ++++ ios/Classes/{ => Models}/NearbyDevice.swift | 2 +- ios/Classes/Models/NearbyMessage.swift | 60 +++++++ ios/Classes/Models/NearbyMessageContent.swift | 149 ++++++++++++++++++ ios/Classes/{ => Models}/NearbySession.swift | 24 ++- ios/Classes/Models/NearbyUserInfo.swift | 58 +++++++ ios/Classes/MyDeviceDataGenerator.swift | 91 ----------- ios/Classes/NearbyManager.swift | 45 ++++-- ios/Classes/NearbyMessageConverter.swift | 59 ------- ios/Classes/NearbyNotificationReceiver.swift | 49 ++++++ ios/Classes/NearbyServicePlugin.swift | 37 ++--- .../{ => Store}/NearbyDevicesStore.swift | 33 ++-- ios/Classes/Store/NearbyFilesStore.swift | 46 ++++++ .../ConnectedDeviceStreamHandler.swift | 8 +- .../NearbyPeersStreamHandler.swift | 2 +- ios/Classes/Utils/Archiver.swift | 56 +++++++ ios/Classes/Utils/Constants.swift | 18 +++ ios/Classes/{ => Utils}/Logger.swift | 0 ios/Classes/Utils/MyDeviceDataGenerator.swift | 39 +++++ lib/src/models/nearby_message_content.dart | 2 +- .../android/models/nearby_device.dart | 5 +- .../socket_service/nearby_socket_service.dart | 4 +- .../platforms/ios/models/nearby_device.dart | 7 +- lib/src/platforms/ios/nearby_ios_service.dart | 43 ++--- .../ios/nearby_service_ios_interface.dart | 4 + .../nearby_service_ios_method_channel.dart | 7 + lib/src/utils/stream_mapper.dart | 27 +++- 29 files changed, 682 insertions(+), 236 deletions(-) create mode 100644 ios/Classes/Models/NearbyCommand.swift rename ios/Classes/{ => Models}/NearbyDevice.swift (98%) create mode 100644 ios/Classes/Models/NearbyMessage.swift create mode 100644 ios/Classes/Models/NearbyMessageContent.swift rename ios/Classes/{ => Models}/NearbySession.swift (68%) create mode 100644 ios/Classes/Models/NearbyUserInfo.swift delete mode 100644 ios/Classes/MyDeviceDataGenerator.swift delete mode 100644 ios/Classes/NearbyMessageConverter.swift create mode 100644 ios/Classes/NearbyNotificationReceiver.swift rename ios/Classes/{ => Store}/NearbyDevicesStore.swift (96%) create mode 100644 ios/Classes/Store/NearbyFilesStore.swift rename ios/Classes/{ => StreamHandler}/ConnectedDeviceStreamHandler.swift (82%) rename ios/Classes/{ => StreamHandler}/NearbyPeersStreamHandler.swift (91%) create mode 100644 ios/Classes/Utils/Archiver.swift create mode 100644 ios/Classes/Utils/Constants.swift rename ios/Classes/{ => Utils}/Logger.swift (100%) create mode 100644 ios/Classes/Utils/MyDeviceDataGenerator.swift diff --git a/example/lib/main.dart b/example/lib/main.dart index 2e6ead3..b5484aa 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'dart:async'; import 'package:nearby_service/nearby_service.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'components/app_snack_bar.dart'; @@ -395,9 +396,9 @@ class AppService extends ChangeNotifier { final filesListener = NearbyServiceFilesListener( onData: (event) async { final files = []; - final directory = Directory( - 'storage/emulated/0/Download', - ); + final directory = Platform.isAndroid + ? Directory('storage/emulated/0/Download') + : await getApplicationDocumentsDirectory(); for (final nearbyFile in event) { final newFile = await nearbyFile.file.copy( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index f5cec58..d0c97ae 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: nearby_service: path: ../ + path_provider: ^2.1.2 diff --git a/ios/Classes/Models/NearbyCommand.swift b/ios/Classes/Models/NearbyCommand.swift new file mode 100644 index 0000000..0881b2f --- /dev/null +++ b/ios/Classes/Models/NearbyCommand.swift @@ -0,0 +1,35 @@ +// +// NearbyCommand.swift +// nearby_service +// +// Created by Kseniia Nikitina on 4/2/24. +// + +import Foundation + + +class NearbyStartCommand { + + init( id: String, filesCount: Int) { + self.id = id + self.filesCount = filesCount + } + + static func fromUserInfo(userInfo: NearbyUserInfo)-> NearbyStartCommand? { + if let id = userInfo.dictionary["id"] as? String , + let filesCount = userInfo.dictionary["filesCount"] as? Int + { + return NearbyStartCommand( + id: id, filesCount: filesCount + ) + } + return nil + } + + func toDictionary() -> [String: Any] { + return ["id": id, "filesCount": filesCount] + } + + let id: String + let filesCount: Int +} diff --git a/ios/Classes/NearbyDevice.swift b/ios/Classes/Models/NearbyDevice.swift similarity index 98% rename from ios/Classes/NearbyDevice.swift rename to ios/Classes/Models/NearbyDevice.swift index e8dc9f5..8a6507e 100644 --- a/ios/Classes/NearbyDevice.swift +++ b/ios/Classes/Models/NearbyDevice.swift @@ -71,7 +71,7 @@ extension NearbyDevice { return deviceDict } - func toJsonString() -> String? { + func toDartFormat() -> String? { var result: String? do { let jsonData = try JSONSerialization.data(withJSONObject: toDictionary()) diff --git a/ios/Classes/Models/NearbyMessage.swift b/ios/Classes/Models/NearbyMessage.swift new file mode 100644 index 0000000..7f7672d --- /dev/null +++ b/ios/Classes/Models/NearbyMessage.swift @@ -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 + } +} + + diff --git a/ios/Classes/Models/NearbyMessageContent.swift b/ios/Classes/Models/NearbyMessageContent.swift new file mode 100644 index 0000000..97543f8 --- /dev/null +++ b/ios/Classes/Models/NearbyMessageContent.swift @@ -0,0 +1,149 @@ +// +// NearbyMessage.swift +// nearby_service +// +// Created by Kseniia Nikitina on 4/2/24. +// + +import Foundation +import MultipeerConnectivity + +class NearbyMessageContent { + + init(type: MessageContentType) { + self.type = type + } + + 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.text) { + return NearbyMessageTextContent.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; + } + + + func toJson() -> [String : Any] { + return ["type": type.name] + } +} + +class NearbyMessageTextContent : NearbyMessageContent { + init(value: String) { + self.value = value + super.init(type: MessageContentType.text) + } + + static func fromJson(json:[String: Any]) -> NearbyMessageTextContent? { + if let message: String = json["value"] as? String { + return NearbyMessageTextContent(value: message) + } + return nil; + } + + override func toJson() -> [String : Any] { + return ["value": value].merging( super.toJson()) { (current, _) in current} + } + + let value: String +} + +class NearbyMessageFilesContent : NearbyMessageContent { + + init(files: Array, id: String, type: MessageContentType) { + self.files = files + self.id = id + super.init(type: type) + } + + static func fromJsonRaw(type: MessageContentType, json: [String: Any]) -> NearbyMessageFilesContent? { + if let filesObjects: Array = json["files"] as? Array> { + let files : [String]? = filesObjects.map({ $0["path"] as? String }).compactMap({$0}) + if let id: String = json["id"] as? String{ + if let requireFiles = files { + return NearbyMessageFilesContent(files: requireFiles, id: id, type: type) + } + } + } + return nil; + } + + override func toJson() -> [String : Any] { + return [ + "files": files.map{["path": $0]}, + "id": id, + ].merging( super.toJson()) { (current, _) in current} + } + + let files: Array + let id: String +} + +class NearbyMessageFilesRequest : NearbyMessageFilesContent { + init(files: Array, id: String) { + super.init(files: files, id: id, type: MessageContentType.filesRequest) + } + static func fromJson( json: [String: Any]) -> NearbyMessageFilesRequest? { + let message = NearbyMessageFilesContent.fromJsonRaw(type: MessageContentType.filesRequest, json: json) + if let requireMessage = message { + return NearbyMessageFilesRequest(files: requireMessage.files, id: requireMessage.id) + } + return nil + } +} +class NearbyMessageFilesResponse : NearbyMessageFilesContent { + init(files: Array, id: String, response: Bool) { + self.response = response + super.init(files: files, id: id, type: MessageContentType.filesResponse) + } + static func fromJson( json: [String: Any]) -> NearbyMessageFilesResponse? { + let message = NearbyMessageFilesContent.fromJsonRaw(type: MessageContentType.filesResponse, json: json) + if let requireMessage = message, + let response = json["response"] as? Bool { + return NearbyMessageFilesResponse(files: requireMessage.files, id: requireMessage.id, response: response) + } + return nil + } + override func toJson() -> [String : Any] { + return [ + "response": response + ].merging( super.toJson()) { (current, _) in current} + } + + let response: Bool +} + + +enum MessageContentType { + case text + case filesRequest + case filesResponse + + static func fromString(value: String) -> MessageContentType { + if (value == text.name) { + return text + } else if (value == filesRequest.name) { + return filesRequest + } else if (value == filesResponse.name) { + return filesResponse + } else { + return text + } + } + + var name : String { + switch self { + case .text: return "text" + case .filesRequest: return "filesRequest" + case .filesResponse: return "filesResponse" + } + } +} diff --git a/ios/Classes/NearbySession.swift b/ios/Classes/Models/NearbySession.swift similarity index 68% rename from ios/Classes/NearbySession.swift rename to ios/Classes/Models/NearbySession.swift index 35953d8..fadc7ca 100644 --- a/ios/Classes/NearbySession.swift +++ b/ios/Classes/Models/NearbySession.swift @@ -27,23 +27,39 @@ class NearbySession: NSObject { 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: ["from": peerID, "data": data] + 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, 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 } + + let destinationURL = localURL.deletingLastPathComponent().appendingPathComponent("\(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() + ) } } diff --git a/ios/Classes/Models/NearbyUserInfo.swift b/ios/Classes/Models/NearbyUserInfo.swift new file mode 100644 index 0000000..c36c9e6 --- /dev/null +++ b/ios/Classes/Models/NearbyUserInfo.swift @@ -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] +} diff --git a/ios/Classes/MyDeviceDataGenerator.swift b/ios/Classes/MyDeviceDataGenerator.swift deleted file mode 100644 index 592a451..0000000 --- a/ios/Classes/MyDeviceDataGenerator.swift +++ /dev/null @@ -1,91 +0,0 @@ -import Foundation -import MultipeerConnectivity - -let SERVICE_TYPE = "mp-connection" -let PEER_ID = "PEER-ID" -let DEVICE_NAME = "DEVICE-NAME" -let ON_MESSAGE_RECEIVED = Notification.Name("NearbySessionOnMessageReceived") - -class MyDeviceDataGenerator { - static func generate(name: String?) -> NearbyDevice { - return NearbyDevice( - peerID: ArchivedData.getPeerID(), - name: ArchivedData.getRequireName(from: name), - deviceType: UIDevice.current.model, - os: UIDevice.current.systemName, - osVersion: UIDevice.current.systemVersion - ) - } - -} - - -class ArchivedData { - static func getPeerID() -> MCPeerID { - if let archivedPeerID = getArchivedPeerID() { - return archivedPeerID - } else { - let peerID = MCPeerID( - displayName: UIDevice.current.name.replacingOccurrences(of: " ", with: "_") - + "_" - + String(Int.random(in: 1000..<50000)) - ) - savePeerID(for: peerID) - return peerID - } - } - - static func getRequireName(from name: String?) -> String { - let archivedName = ArchivedData.getArchivedName() - if let newName = name { - if (archivedName != newName) { - ArchivedData.saveName(for: newName) - } - return newName - } - return archivedName ?? UIDevice.current.name - } - - static func getArchivedPeerID() -> 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 getArchivedName() -> 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) - } - } - } -} diff --git a/ios/Classes/NearbyManager.swift b/ios/Classes/NearbyManager.swift index a59b8ee..f738bd6 100644 --- a/ios/Classes/NearbyManager.swift +++ b/ios/Classes/NearbyManager.swift @@ -26,11 +26,11 @@ class NearbyManager: NSObject { } func getSavedDeviceName(result: @escaping FlutterResult) { - result(ArchivedData.getArchivedName()) + result(Archiver.getName()) } func getCurrentDevice(result: @escaping FlutterResult) { - result(device.toJsonString()) + result(device.toDartFormat()) } func openServicesSettings(result: @escaping FlutterResult) { @@ -65,7 +65,7 @@ class NearbyManager: NSObject { } func getPeers(result: @escaping FlutterResult) { - result(NearbyDevicesStore.instance.getDevicesToJsonString()) + result(NearbyDevicesStore.instance.toDartFormat()) } func invite(for deviceId: String, result: @escaping FlutterResult) { @@ -101,17 +101,14 @@ class NearbyManager: NSObject { result(true) } - func send(for message: String, with receiverId: String, result: @escaping FlutterResult) { + func send(for content: NearbyMessageContent, with receiverId: String, result: @escaping FlutterResult) { let device = NearbyDevicesStore.instance.find(for: receiverId) do { if let requireDevice = device { - let data = [ - "name": self.device.name, - "message": message - ] + let message = NearbyMessage(content: content, senderName: self.device.name, senderPeerID: self.device.peerID) try requireDevice.session?.session?.send( - try JSONSerialization.data(withJSONObject: data), + try JSONSerialization.data(withJSONObject: message.toDictionary()), toPeers: [requireDevice.peerID], with: MCSessionSendDataMode.reliable ) @@ -123,7 +120,34 @@ class NearbyManager: NSObject { result(true) } - + func sendFiles(id: String, paths: [String], with receiverId: String) { + do { + let device = NearbyDevicesStore.instance.find(for: receiverId) + if let requireDevice = device { + try requireDevice.session?.session?.send( + try JSONSerialization.data(withJSONObject: NearbyStartCommand( id: id, filesCount: paths.count).toDictionary()), + 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) + } + } } extension NearbyManager: MCNearbyServiceAdvertiserDelegate { @@ -139,6 +163,7 @@ extension NearbyManager: MCNearbyServiceAdvertiserDelegate { self.invitationHandlers[peerID.displayName] = invitationHandler } + } extension NearbyManager: MCNearbyServiceBrowserDelegate { diff --git a/ios/Classes/NearbyMessageConverter.swift b/ios/Classes/NearbyMessageConverter.swift deleted file mode 100644 index 5aae884..0000000 --- a/ios/Classes/NearbyMessageConverter.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// NearbyMessageConverter.swift -// nearby_service -// -// Created by Kseniia Nikitina on 29/1/24. -// - -import Foundation -import MultipeerConnectivity - -class NearbyMessageConverter { - static func convert(userInfo: [AnyHashable : Any]?) -> String? { - if let data = getMessageData(userInfo: userInfo), - let peerID = getMessagePeerID(userInfo: userInfo) { - return createMessage(data: data, peerID: peerID) - } - return nil - } - - static private func getMessageData(userInfo: [AnyHashable : Any]?) -> [String: String]? { - do { - if let data = userInfo?["data"] as? Data, - let dictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] { - return dictionary - } - } catch let error { - Logger.error(message: error.localizedDescription) - } - return nil - } - - static private func getMessagePeerID(userInfo: [AnyHashable:Any]?) -> MCPeerID? { - return userInfo?["from"] as? MCPeerID - - } - - static private func createMessage(data: [String: String], peerID: MCPeerID)-> String? { - do { - if let message = data["message"], - let name = data["name"] { - - var result: String? - let jsonObject = [ - "content": ["value": message, "type": "text"], - "sender": ["id": peerID.displayName, "displayName": name], - ] as [String : Any] - - let jsonData = try JSONSerialization.data(withJSONObject: jsonObject) - if let jsonString = String(data: jsonData, encoding: .utf8) { - result = jsonString - } - return result - } - } catch let error { - Logger.error(message: error.localizedDescription) - } - return nil - } -} diff --git a/ios/Classes/NearbyNotificationReceiver.swift b/ios/Classes/NearbyNotificationReceiver.swift new file mode 100644 index 0000000..820d0e5 --- /dev/null +++ b/ios/Classes/NearbyNotificationReceiver.swift @@ -0,0 +1,49 @@ +// +// NearbyServicePluginOnReceived.swift +// nearby_service +// +// Created by Kseniia Nikitina on 4/2/24. +// + +import Foundation +import Flutter + +extension NearbyServicePlugin { + @objc func onMessageReceived(notification: Notification) { + DispatchQueue.main.async { + if let userInfo = NearbyUserInfo.fromDictionary(userInfo: notification.userInfo) { + if let message = NearbyMessage.fromUserInfo(userInfo: userInfo) { + if message.content is NearbyMessageFilesResponse { + let response = message.content as! NearbyMessageFilesResponse + if (response.response) { + self.manager.sendFiles( + id: response.id, + paths: response.files, + with: message.senderPeerID.displayName + ) + } + } + 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()) + } + } + } + + } + } +} diff --git a/ios/Classes/NearbyServicePlugin.swift b/ios/Classes/NearbyServicePlugin.swift index 296156c..fb2ab54 100644 --- a/ios/Classes/NearbyServicePlugin.swift +++ b/ios/Classes/NearbyServicePlugin.swift @@ -2,14 +2,16 @@ import Flutter import MultipeerConnectivity import UIKit -public class NearbyServicePlugin: NSObject, FlutterPlugin { +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) { @@ -34,15 +36,20 @@ public class NearbyServicePlugin: NSObject, FlutterPlugin { let manager = NearbyManager() let instance = NearbyServicePlugin(manager: manager, channel: channel) - registrar.addMethodCallDelegate(instance, 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) { @@ -52,18 +59,14 @@ public class NearbyServicePlugin: NSObject, FlutterPlugin { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - + switch call.method { case "getPlatformVersion": result("iOS " + UIDevice.current.systemVersion) case "getPlatformModel": result(UIDevice.current.name) case "initialize": - if let deviceName: String = getArgument(for: "deviceName", call: call) { - manager.initialize(for: deviceName, result: result) - } else { - manager.initialize(for: nil, result: result) - } + manager.initialize(for: getArgument(for: "deviceName", call: call), result: result) case "getSavedDeviceName": manager.getSavedDeviceName(result: result) case "getCurrentDevice": @@ -102,12 +105,12 @@ public class NearbyServicePlugin: NSObject, FlutterPlugin { } case "send": - if let content: Dictionary = getArgument(for: "content", call: call), - let message: String = content["value"] as? String { + if let contentJson: Dictionary = getArgument(for: "content", call: call), + let content: NearbyMessageContent = NearbyMessageContent.typedFromJson(json: contentJson) { if let receiver : Dictionary = getArgument(for: "receiver", call: call), let receiverId : String = receiver["id"] as? String { - manager.send(for: message, with: receiverId, result: result) + manager.send(for: content, with: receiverId, result: result) } else { result(false) } @@ -119,14 +122,7 @@ public class NearbyServicePlugin: NSObject, FlutterPlugin { result(FlutterMethodNotImplemented) } } - @objc func onMessageReceived(notification: Notification) { - DispatchQueue.main.async { - let result = NearbyMessageConverter.convert(userInfo: notification.userInfo) - self.channel.invokeMethod("invoke_nearby_service_message_received", arguments: result) - } - } - - private func getArgument(for name: String, call: FlutterMethodCall) -> T? { + func getArgument(for name: String, call: FlutterMethodCall) -> T? { guard let data = call.arguments as? Dictionary else { return nil } @@ -136,3 +132,4 @@ public class NearbyServicePlugin: NSObject, FlutterPlugin { return argument } } + diff --git a/ios/Classes/NearbyDevicesStore.swift b/ios/Classes/Store/NearbyDevicesStore.swift similarity index 96% rename from ios/Classes/NearbyDevicesStore.swift rename to ios/Classes/Store/NearbyDevicesStore.swift index a89c379..18d112e 100644 --- a/ios/Classes/NearbyDevicesStore.swift +++ b/ios/Classes/Store/NearbyDevicesStore.swift @@ -22,22 +22,7 @@ class NearbyDevicesStore : NSObject { return device.peerID.displayName == deviceId } } - - func getDevicesToJsonString() -> 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 "[]" - } - + func add(for peerID: MCPeerID, discoveryInfo: [String: String]? = nil) -> NearbyDevice? { devices = devices.filter{$0.peerID.displayName != peerID.displayName} @@ -55,4 +40,20 @@ class NearbyDevicesStore : NSObject { 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 "[]" + } + } diff --git a/ios/Classes/Store/NearbyFilesStore.swift b/ios/Classes/Store/NearbyFilesStore.swift new file mode 100644 index 0000000..b043c7e --- /dev/null +++ b/ios/Classes/Store/NearbyFilesStore.swift @@ -0,0 +1,46 @@ +// +// NearbyFilesStore.swift +// nearby_service +// +// Created by Kseniia Nikitina on 3/2/24. +// + +import Foundation + +class NearbyFilesStore { + static let instance = NearbyFilesStore() + + private var paths : [String] = [] + private var id: String? = nil + private var maxCount: Int = 0 + private var count: Int = 0 + + func startReceiving(command: NearbyStartCommand) { + self.paths.removeAll() + self.id = command.id + 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 toDartFormat() -> String? { + let pathsObject = paths.map { ["path": $0]} + do { + let jsonData = try JSONSerialization.data(withJSONObject: pathsObject) + if let jsonString = String(data: jsonData, encoding: .utf8) { + return jsonString + } + } catch { + return "[]" + } + return "[]" + } +} diff --git a/ios/Classes/ConnectedDeviceStreamHandler.swift b/ios/Classes/StreamHandler/ConnectedDeviceStreamHandler.swift similarity index 82% rename from ios/Classes/ConnectedDeviceStreamHandler.swift rename to ios/Classes/StreamHandler/ConnectedDeviceStreamHandler.swift index 49749b3..cad0432 100644 --- a/ios/Classes/ConnectedDeviceStreamHandler.swift +++ b/ios/Classes/StreamHandler/ConnectedDeviceStreamHandler.swift @@ -1,4 +1,5 @@ import Flutter +import MultipeerConnectivity class ConnectedDeviceStreamHandler: NSObject, FlutterStreamHandler { private var eventSink: FlutterEventSink? @@ -21,8 +22,11 @@ class ConnectedDeviceStreamHandler: NSObject, FlutterStreamHandler { 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) { - result = device.toJsonString() + if let device = NearbyDevicesStore.instance.find(for: deviceId), + let session = device.session { + if (session.state == MCSessionState.connected) { + result = device.toDartFormat() + } } self.eventSink?(result) } diff --git a/ios/Classes/NearbyPeersStreamHandler.swift b/ios/Classes/StreamHandler/NearbyPeersStreamHandler.swift similarity index 91% rename from ios/Classes/NearbyPeersStreamHandler.swift rename to ios/Classes/StreamHandler/NearbyPeersStreamHandler.swift index 5738599..afba29d 100644 --- a/ios/Classes/NearbyPeersStreamHandler.swift +++ b/ios/Classes/StreamHandler/NearbyPeersStreamHandler.swift @@ -20,7 +20,7 @@ class NearbyPeersStreamHandler: NSObject, FlutterStreamHandler { self.timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in guard let self = self else { return } - let devicesList = NearbyDevicesStore.instance.getDevicesToJsonString() + let devicesList = NearbyDevicesStore.instance.toDartFormat() self.eventSink?(devicesList) } } diff --git a/ios/Classes/Utils/Archiver.swift b/ios/Classes/Utils/Archiver.swift new file mode 100644 index 0000000..d37a551 --- /dev/null +++ b/ios/Classes/Utils/Archiver.swift @@ -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) + } + } + } +} diff --git a/ios/Classes/Utils/Constants.swift b/ios/Classes/Utils/Constants.swift new file mode 100644 index 0000000..5629041 --- /dev/null +++ b/ios/Classes/Utils/Constants.swift @@ -0,0 +1,18 @@ +// +// 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" diff --git a/ios/Classes/Logger.swift b/ios/Classes/Utils/Logger.swift similarity index 100% rename from ios/Classes/Logger.swift rename to ios/Classes/Utils/Logger.swift diff --git a/ios/Classes/Utils/MyDeviceDataGenerator.swift b/ios/Classes/Utils/MyDeviceDataGenerator.swift new file mode 100644 index 0000000..1f6b980 --- /dev/null +++ b/ios/Classes/Utils/MyDeviceDataGenerator.swift @@ -0,0 +1,39 @@ +import Foundation +import MultipeerConnectivity + + +class MyDeviceDataGenerator { + static func generate(name: String?) -> NearbyDevice { + return NearbyDevice( + peerID: getPeerID(), + name: getNameArchived(or: name), + deviceType: UIDevice.current.model, + os: UIDevice.current.systemName, + osVersion: UIDevice.current.systemVersion + ) + } + static private func getPeerID() -> MCPeerID { + if let archivedPeerID = Archiver.getPeerID() { + return archivedPeerID + } else { + let peerID = MCPeerID( + displayName: UIDevice.current.name.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 + } + return archivedName ?? UIDevice.current.name + } + +} diff --git a/lib/src/models/nearby_message_content.dart b/lib/src/models/nearby_message_content.dart index 9a39dd6..e677891 100644 --- a/lib/src/models/nearby_message_content.dart +++ b/lib/src/models/nearby_message_content.dart @@ -327,4 +327,4 @@ class NearbyMessageFilesResponse extends NearbyMessageFilesContent { String toString() { return 'NearbyMessageFileResponse{response: $response, id: $id, files: $files}'; } -} \ No newline at end of file +} diff --git a/lib/src/platforms/android/models/nearby_device.dart b/lib/src/platforms/android/models/nearby_device.dart index 2bd0441..f11ba1a 100644 --- a/lib/src/platforms/android/models/nearby_device.dart +++ b/lib/src/platforms/android/models/nearby_device.dart @@ -150,6 +150,9 @@ class NearbyAndroidMapper implements NearbyDeviceMapper { @override NearbyDevice? mapToDevice(dynamic value) { - return NearbyAndroidDevice.fromJson(JSONDecoder.decodeMap(value)); + final decoded = JSONDecoder.decodeMap(value); + if (decoded == null) return null; + + return NearbyAndroidDevice.fromJson(decoded); } } diff --git a/lib/src/platforms/android/socket_service/nearby_socket_service.dart b/lib/src/platforms/android/socket_service/nearby_socket_service.dart index 38a1cae..00e4ec3 100644 --- a/lib/src/platforms/android/socket_service/nearby_socket_service.dart +++ b/lib/src/platforms/android/socket_service/nearby_socket_service.dart @@ -204,8 +204,8 @@ class NearbySocketService { if (_connectedDeviceId != null) { _messagesSubscription = _socket - ?.map(MessagesStreamMapper.toMessage) - .where((e) => e != null) + ?.where((e) => e != null) + .map(MessagesStreamMapper.toMessage) .cast() .map((e) => MessagesStreamMapper.replaceId(e, _connectedDeviceId!)) .listen( diff --git a/lib/src/platforms/ios/models/nearby_device.dart b/lib/src/platforms/ios/models/nearby_device.dart index cf48d71..0b46db6 100644 --- a/lib/src/platforms/ios/models/nearby_device.dart +++ b/lib/src/platforms/ios/models/nearby_device.dart @@ -85,8 +85,9 @@ class NearbyIOSMapper implements NearbyDeviceMapper { @override NearbyDevice? mapToDevice(dynamic value) { - return NearbyIOSDevice.fromJson( - JSONDecoder.decodeMap(value), - ); + final decoded = JSONDecoder.decodeMap(value); + if (decoded == null) return null; + + return NearbyIOSDevice.fromJson(decoded); } } diff --git a/lib/src/platforms/ios/nearby_ios_service.dart b/lib/src/platforms/ios/nearby_ios_service.dart index a102b79..717fe2b 100644 --- a/lib/src/platforms/ios/nearby_ios_service.dart +++ b/lib/src/platforms/ios/nearby_ios_service.dart @@ -17,6 +17,7 @@ class NearbyIOSService extends NearbyService { final _state = ValueNotifier(CommunicationChannelState.notConnected); StreamSubscription? _messagesSubscription; + StreamSubscription? _resourcesSubscription; @override ValueListenable get communicationChannelState => @@ -166,25 +167,12 @@ class NearbyIOSService extends NearbyService { final eventListener = data.messagesListener; final filesListener = data.filesListener; _messagesSubscription = NearbyServiceIOSPlatform.instance.messagesStream - // .map(MessagesStreamMapper.toMessage) - // .where((event) => event?.sender.id == data.connectedDeviceId) - // .where((event) => event != null) - // .cast() + .where((event) => event != null) + .map(MessagesStreamMapper.toMessage) + .where((event) => event?.sender.id == data.connectedDeviceId) + .cast() .listen( - (event) { - try { - final message = MessagesStreamMapper.toMessage(event); - if (message != null && message.sender.id == data.connectedDeviceId) { - eventListener.onData(message); - } - } catch (e) { - try { - filesListener?.onData(event); - } catch (e) { - Logger.error(e); - } - } - }, + eventListener.onData, onDone: () { _state.value = CommunicationChannelState.notConnected; eventListener.onDone?.call(); @@ -196,6 +184,20 @@ class NearbyIOSService extends NearbyService { }, cancelOnError: eventListener.cancelOnError, ); + _resourcesSubscription = NearbyServiceIOSPlatform.instance.resourcesStream + .map(ResourcesStreamMapper.toFiles) + .where((event) => event != null) + .cast>() + .listen( + (e) => filesListener?.onData.call(e), + onDone: filesListener?.onDone, + onError: (e, s) { + Logger.error(e); + _state.value = CommunicationChannelState.notConnected; + filesListener?.onError?.call(e, s); + }, + cancelOnError: filesListener?.cancelOnError, + ); if (_messagesSubscription != null) { Logger.info('Messages subscription was created successfully'); eventListener.onCreated?.call(); @@ -203,6 +205,9 @@ class NearbyIOSService extends NearbyService { } else { _state.value = CommunicationChannelState.notConnected; } + if (_resourcesSubscription != null) { + Logger.info('Resources subscription was created successfully'); + } return true; } @@ -214,7 +219,9 @@ class NearbyIOSService extends NearbyService { @override FutureOr endCommunicationChannel() async { await _messagesSubscription?.cancel(); + await _resourcesSubscription?.cancel(); _messagesSubscription = null; + _resourcesSubscription = null; _state.value = CommunicationChannelState.notConnected; Logger.debug('Communication channel was cancelled'); return true; diff --git a/lib/src/platforms/ios/nearby_service_ios_interface.dart b/lib/src/platforms/ios/nearby_service_ios_interface.dart index 1d36778..9982814 100644 --- a/lib/src/platforms/ios/nearby_service_ios_interface.dart +++ b/lib/src/platforms/ios/nearby_service_ios_interface.dart @@ -27,6 +27,10 @@ abstract class NearbyServiceIOSPlatform extends PlatformInterface { throw UnimplementedError('messagesStream() has not been implemented.'); } + Stream get resourcesStream { + throw UnimplementedError('resourcesStream() has not been implemented.'); + } + Future initialize([String? deviceName]) { throw UnimplementedError('initialize() has not been implemented.'); } diff --git a/lib/src/platforms/ios/nearby_service_ios_method_channel.dart b/lib/src/platforms/ios/nearby_service_ios_method_channel.dart index de8188f..63b2bc4 100644 --- a/lib/src/platforms/ios/nearby_service_ios_method_channel.dart +++ b/lib/src/platforms/ios/nearby_service_ios_method_channel.dart @@ -7,10 +7,14 @@ import 'package:nearby_service/nearby_service.dart'; /// An implementation of [NearbyServiceIOSPlatform] that uses method channels. class MethodChannelIOSNearbyService extends NearbyServiceIOSPlatform { final messageReceiver = StreamController.broadcast(); + final resourcesReceiver = StreamController.broadcast(); @override Stream get messagesStream => messageReceiver.stream; + @override + Stream get resourcesStream => resourcesReceiver.stream; + /// The method channel used to interact with the native platform. @visibleForTesting final methodChannel = const MethodChannel('nearby_service'); @@ -22,6 +26,9 @@ class MethodChannelIOSNearbyService extends NearbyServiceIOSPlatform { case 'invoke_nearby_service_message_received': messageReceiver.add(handler.arguments); break; + case 'invoke_nearby_service_resources_received': + resourcesReceiver.add(handler.arguments); + break; } }); return (await methodChannel.invokeMethod( diff --git a/lib/src/utils/stream_mapper.dart b/lib/src/utils/stream_mapper.dart index 7da4b11..2e1d772 100644 --- a/lib/src/utils/stream_mapper.dart +++ b/lib/src/utils/stream_mapper.dart @@ -1,8 +1,8 @@ +import 'dart:io'; + import 'package:nearby_service/nearby_service.dart'; import 'package:nearby_service/src/utils/json_decoder.dart'; -import 'logger.dart'; - abstract class MessagesStreamMapper { static ReceivedNearbyMessage replaceId( ReceivedNearbyMessage message, @@ -22,10 +22,29 @@ abstract class MessagesStreamMapper { final decoded = JSONDecoder.decodeMap(event); return ReceivedNearbyMessage.fromJson(decoded); } catch (e) { - Logger.debug( + throw NearbyServiceException( + 'Can\'t convert $event to ReceivedNearbyMessage', + ); + } + } +} + +abstract class ResourcesStreamMapper { + static List? toFiles(dynamic event) { + try { + final decoded = JSONDecoder.decodeList(event); + final infoList = [ + ...?decoded?.map( + (e) => NearbyFileInfo.fromJson(e as Map), + ) + ]; + return [ + ...infoList.map((e) => NearbyFile(info: e, file: File(e.path))), + ]; + } catch (e) { + throw NearbyServiceException( 'Can\'t convert $event to ReceivedNearbyMessage', ); } - return null; } }