feat(ios): files sending

This commit is contained in:
ksenia312
2024-02-04 16:23:48 +01:00
parent 32a9f2a923
commit 66a79eb4ba
29 changed files with 682 additions and 236 deletions
+4 -3
View File
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'dart:async'; import 'dart:async';
import 'package:nearby_service/nearby_service.dart'; import 'package:nearby_service/nearby_service.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'components/app_snack_bar.dart'; import 'components/app_snack_bar.dart';
@@ -395,9 +396,9 @@ class AppService extends ChangeNotifier {
final filesListener = NearbyServiceFilesListener( final filesListener = NearbyServiceFilesListener(
onData: (event) async { onData: (event) async {
final files = <File>[]; final files = <File>[];
final directory = Directory( final directory = Platform.isAndroid
'storage/emulated/0/Download', ? Directory('storage/emulated/0/Download')
); : await getApplicationDocumentsDirectory();
for (final nearbyFile in event) { for (final nearbyFile in event) {
final newFile = await nearbyFile.file.copy( final newFile = await nearbyFile.file.copy(
+1
View File
@@ -15,6 +15,7 @@ dependencies:
nearby_service: nearby_service:
path: ../ path: ../
path_provider: ^2.1.2
+35
View File
@@ -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
}
@@ -71,7 +71,7 @@ extension NearbyDevice {
return deviceDict return deviceDict
} }
func toJsonString() -> String? { func toDartFormat() -> String? {
var result: String? var result: String?
do { do {
let jsonData = try JSONSerialization.data(withJSONObject: toDictionary()) let jsonData = try JSONSerialization.data(withJSONObject: toDictionary())
+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,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<String>, 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<Dictionary<String, AnyObject>> {
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<String>
let id: String
}
class NearbyMessageFilesRequest : NearbyMessageFilesContent {
init(files: Array<String>, 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<String>, 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"
}
}
}
@@ -27,23 +27,39 @@ class NearbySession: NSObject {
extension NearbySession: MCSessionDelegate { extension NearbySession: MCSessionDelegate {
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
self.state = state self.state = state
} }
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
NotificationCenter.default.post( NotificationCenter.default.post(
name: ON_MESSAGE_RECEIVED, name: ON_MESSAGE_RECEIVED,
object: nil, 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, 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?) { 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()
)
} }
} }
+58
View File
@@ -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]
}
-91
View File
@@ -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)
}
}
}
}
+35 -10
View File
@@ -26,11 +26,11 @@ class NearbyManager: NSObject {
} }
func getSavedDeviceName(result: @escaping FlutterResult) { func getSavedDeviceName(result: @escaping FlutterResult) {
result(ArchivedData.getArchivedName()) result(Archiver.getName())
} }
func getCurrentDevice(result: @escaping FlutterResult) { func getCurrentDevice(result: @escaping FlutterResult) {
result(device.toJsonString()) result(device.toDartFormat())
} }
func openServicesSettings(result: @escaping FlutterResult) { func openServicesSettings(result: @escaping FlutterResult) {
@@ -65,7 +65,7 @@ class NearbyManager: NSObject {
} }
func getPeers(result: @escaping FlutterResult) { func getPeers(result: @escaping FlutterResult) {
result(NearbyDevicesStore.instance.getDevicesToJsonString()) result(NearbyDevicesStore.instance.toDartFormat())
} }
func invite(for deviceId: String, result: @escaping FlutterResult) { func invite(for deviceId: String, result: @escaping FlutterResult) {
@@ -101,17 +101,14 @@ class NearbyManager: NSObject {
result(true) 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) let device = NearbyDevicesStore.instance.find(for: receiverId)
do { do {
if let requireDevice = device { if let requireDevice = device {
let data = [ let message = NearbyMessage(content: content, senderName: self.device.name, senderPeerID: self.device.peerID)
"name": self.device.name,
"message": message
]
try requireDevice.session?.session?.send( try requireDevice.session?.session?.send(
try JSONSerialization.data(withJSONObject: data), try JSONSerialization.data(withJSONObject: message.toDictionary()),
toPeers: [requireDevice.peerID], toPeers: [requireDevice.peerID],
with: MCSessionSendDataMode.reliable with: MCSessionSendDataMode.reliable
) )
@@ -123,7 +120,34 @@ class NearbyManager: NSObject {
result(true) 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 { extension NearbyManager: MCNearbyServiceAdvertiserDelegate {
@@ -139,6 +163,7 @@ extension NearbyManager: MCNearbyServiceAdvertiserDelegate {
self.invitationHandlers[peerID.displayName] = invitationHandler self.invitationHandlers[peerID.displayName] = invitationHandler
} }
} }
extension NearbyManager: MCNearbyServiceBrowserDelegate { extension NearbyManager: MCNearbyServiceBrowserDelegate {
-59
View File
@@ -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
}
}
@@ -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())
}
}
}
}
}
}
+17 -20
View File
@@ -2,14 +2,16 @@ import Flutter
import MultipeerConnectivity import MultipeerConnectivity
import UIKit import UIKit
public class NearbyServicePlugin: NSObject, FlutterPlugin { public class NearbyServicePlugin: NSObject, FlutterPlugin{
let manager: NearbyManager; let manager: NearbyManager;
let channel: FlutterMethodChannel let channel: FlutterMethodChannel
init(manager: NearbyManager, channel: FlutterMethodChannel) { init(manager: NearbyManager, channel: FlutterMethodChannel) {
self.manager = manager self.manager = manager
self.channel = channel self.channel = channel
} }
public static func register(with registrar: FlutterPluginRegistrar) { public static func register(with registrar: FlutterPluginRegistrar) {
@@ -34,15 +36,20 @@ public class NearbyServicePlugin: NSObject, FlutterPlugin {
let manager = NearbyManager() let manager = NearbyManager()
let instance = NearbyServicePlugin(manager: manager, channel: channel) let instance = NearbyServicePlugin(manager: manager, channel: channel)
registrar.addMethodCallDelegate(instance, channel: channel)
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
instance, instance,
selector: #selector(onMessageReceived), selector: #selector(onMessageReceived),
name: ON_MESSAGE_RECEIVED, name: ON_MESSAGE_RECEIVED,
object: nil 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) { public func detachFromEngine(for registrar: FlutterPluginRegistrar) {
@@ -52,18 +59,14 @@ public class NearbyServicePlugin: NSObject, FlutterPlugin {
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method { switch call.method {
case "getPlatformVersion": case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion) result("iOS " + UIDevice.current.systemVersion)
case "getPlatformModel": case "getPlatformModel":
result(UIDevice.current.name) result(UIDevice.current.name)
case "initialize": case "initialize":
if let deviceName: String = getArgument(for: "deviceName", call: call) { manager.initialize(for: getArgument(for: "deviceName", call: call), result: result)
manager.initialize(for: deviceName, result: result)
} else {
manager.initialize(for: nil, result: result)
}
case "getSavedDeviceName": case "getSavedDeviceName":
manager.getSavedDeviceName(result: result) manager.getSavedDeviceName(result: result)
case "getCurrentDevice": case "getCurrentDevice":
@@ -102,12 +105,12 @@ public class NearbyServicePlugin: NSObject, FlutterPlugin {
} }
case "send": case "send":
if let content: Dictionary<String, AnyObject> = getArgument(for: "content", call: call), if let contentJson: Dictionary<String, AnyObject> = getArgument(for: "content", call: call),
let message: String = content["value"] as? String { let content: NearbyMessageContent = NearbyMessageContent.typedFromJson(json: contentJson) {
if let receiver : Dictionary<String, AnyObject> = getArgument(for: "receiver", call: call), if let receiver : Dictionary<String, AnyObject> = getArgument(for: "receiver", call: call),
let receiverId : String = receiver["id"] as? String let receiverId : String = receiver["id"] as? String
{ {
manager.send(for: message, with: receiverId, result: result) manager.send(for: content, with: receiverId, result: result)
} else { } else {
result(false) result(false)
} }
@@ -119,14 +122,7 @@ public class NearbyServicePlugin: NSObject, FlutterPlugin {
result(FlutterMethodNotImplemented) result(FlutterMethodNotImplemented)
} }
} }
@objc func onMessageReceived(notification: Notification) { func getArgument<T>(for name: String, call: FlutterMethodCall) -> T? {
DispatchQueue.main.async {
let result = NearbyMessageConverter.convert(userInfo: notification.userInfo)
self.channel.invokeMethod("invoke_nearby_service_message_received", arguments: result)
}
}
private func getArgument<T>(for name: String, call: FlutterMethodCall) -> T? {
guard let data = call.arguments as? Dictionary<String, AnyObject> else { guard let data = call.arguments as? Dictionary<String, AnyObject> else {
return nil return nil
} }
@@ -136,3 +132,4 @@ public class NearbyServicePlugin: NSObject, FlutterPlugin {
return argument return argument
} }
} }
@@ -22,22 +22,7 @@ class NearbyDevicesStore : NSObject {
return device.peerID.displayName == deviceId 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? { func add(for peerID: MCPeerID, discoveryInfo: [String: String]? = nil) -> NearbyDevice? {
devices = devices.filter{$0.peerID.displayName != peerID.displayName} devices = devices.filter{$0.peerID.displayName != peerID.displayName}
@@ -55,4 +40,20 @@ class NearbyDevicesStore : NSObject {
func clear() { func clear() {
self.devices = [] 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 "[]"
}
} }
+46
View File
@@ -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 "[]"
}
}
@@ -1,4 +1,5 @@
import Flutter import Flutter
import MultipeerConnectivity
class ConnectedDeviceStreamHandler: NSObject, FlutterStreamHandler { class ConnectedDeviceStreamHandler: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink? private var eventSink: FlutterEventSink?
@@ -21,8 +22,11 @@ class ConnectedDeviceStreamHandler: NSObject, FlutterStreamHandler {
self.timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in self.timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
var result: String? var result: String?
if let device = NearbyDevicesStore.instance.find(for: deviceId) { if let device = NearbyDevicesStore.instance.find(for: deviceId),
result = device.toJsonString() let session = device.session {
if (session.state == MCSessionState.connected) {
result = device.toDartFormat()
}
} }
self.eventSink?(result) self.eventSink?(result)
} }
@@ -20,7 +20,7 @@ class NearbyPeersStreamHandler: NSObject, FlutterStreamHandler {
self.timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in self.timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
let devicesList = NearbyDevicesStore.instance.getDevicesToJsonString() let devicesList = NearbyDevicesStore.instance.toDartFormat()
self.eventSink?(devicesList) self.eventSink?(devicesList)
} }
} }
+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)
}
}
}
}
+18
View File
@@ -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"
@@ -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
}
}
+1 -1
View File
@@ -327,4 +327,4 @@ class NearbyMessageFilesResponse extends NearbyMessageFilesContent {
String toString() { String toString() {
return 'NearbyMessageFileResponse{response: $response, id: $id, files: $files}'; return 'NearbyMessageFileResponse{response: $response, id: $id, files: $files}';
} }
} }
@@ -150,6 +150,9 @@ class NearbyAndroidMapper implements NearbyDeviceMapper {
@override @override
NearbyDevice? mapToDevice(dynamic value) { NearbyDevice? mapToDevice(dynamic value) {
return NearbyAndroidDevice.fromJson(JSONDecoder.decodeMap(value)); final decoded = JSONDecoder.decodeMap(value);
if (decoded == null) return null;
return NearbyAndroidDevice.fromJson(decoded);
} }
} }
@@ -204,8 +204,8 @@ class NearbySocketService {
if (_connectedDeviceId != null) { if (_connectedDeviceId != null) {
_messagesSubscription = _socket _messagesSubscription = _socket
?.map(MessagesStreamMapper.toMessage) ?.where((e) => e != null)
.where((e) => e != null) .map(MessagesStreamMapper.toMessage)
.cast<ReceivedNearbyMessage>() .cast<ReceivedNearbyMessage>()
.map((e) => MessagesStreamMapper.replaceId(e, _connectedDeviceId!)) .map((e) => MessagesStreamMapper.replaceId(e, _connectedDeviceId!))
.listen( .listen(
@@ -85,8 +85,9 @@ class NearbyIOSMapper implements NearbyDeviceMapper {
@override @override
NearbyDevice? mapToDevice(dynamic value) { NearbyDevice? mapToDevice(dynamic value) {
return NearbyIOSDevice.fromJson( final decoded = JSONDecoder.decodeMap(value);
JSONDecoder.decodeMap(value), if (decoded == null) return null;
);
return NearbyIOSDevice.fromJson(decoded);
} }
} }
+25 -18
View File
@@ -17,6 +17,7 @@ class NearbyIOSService extends NearbyService {
final _state = ValueNotifier(CommunicationChannelState.notConnected); final _state = ValueNotifier(CommunicationChannelState.notConnected);
StreamSubscription? _messagesSubscription; StreamSubscription? _messagesSubscription;
StreamSubscription? _resourcesSubscription;
@override @override
ValueListenable<CommunicationChannelState> get communicationChannelState => ValueListenable<CommunicationChannelState> get communicationChannelState =>
@@ -166,25 +167,12 @@ class NearbyIOSService extends NearbyService {
final eventListener = data.messagesListener; final eventListener = data.messagesListener;
final filesListener = data.filesListener; final filesListener = data.filesListener;
_messagesSubscription = NearbyServiceIOSPlatform.instance.messagesStream _messagesSubscription = NearbyServiceIOSPlatform.instance.messagesStream
// .map(MessagesStreamMapper.toMessage) .where((event) => event != null)
// .where((event) => event?.sender.id == data.connectedDeviceId) .map(MessagesStreamMapper.toMessage)
// .where((event) => event != null) .where((event) => event?.sender.id == data.connectedDeviceId)
// .cast<ReceivedNearbyMessage>() .cast<ReceivedNearbyMessage>()
.listen( .listen(
(event) { eventListener.onData,
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);
}
}
},
onDone: () { onDone: () {
_state.value = CommunicationChannelState.notConnected; _state.value = CommunicationChannelState.notConnected;
eventListener.onDone?.call(); eventListener.onDone?.call();
@@ -196,6 +184,20 @@ class NearbyIOSService extends NearbyService {
}, },
cancelOnError: eventListener.cancelOnError, cancelOnError: eventListener.cancelOnError,
); );
_resourcesSubscription = NearbyServiceIOSPlatform.instance.resourcesStream
.map(ResourcesStreamMapper.toFiles)
.where((event) => event != null)
.cast<List<NearbyFile>>()
.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) { if (_messagesSubscription != null) {
Logger.info('Messages subscription was created successfully'); Logger.info('Messages subscription was created successfully');
eventListener.onCreated?.call(); eventListener.onCreated?.call();
@@ -203,6 +205,9 @@ class NearbyIOSService extends NearbyService {
} else { } else {
_state.value = CommunicationChannelState.notConnected; _state.value = CommunicationChannelState.notConnected;
} }
if (_resourcesSubscription != null) {
Logger.info('Resources subscription was created successfully');
}
return true; return true;
} }
@@ -214,7 +219,9 @@ class NearbyIOSService extends NearbyService {
@override @override
FutureOr<bool> endCommunicationChannel() async { FutureOr<bool> endCommunicationChannel() async {
await _messagesSubscription?.cancel(); await _messagesSubscription?.cancel();
await _resourcesSubscription?.cancel();
_messagesSubscription = null; _messagesSubscription = null;
_resourcesSubscription = null;
_state.value = CommunicationChannelState.notConnected; _state.value = CommunicationChannelState.notConnected;
Logger.debug('Communication channel was cancelled'); Logger.debug('Communication channel was cancelled');
return true; return true;
@@ -27,6 +27,10 @@ abstract class NearbyServiceIOSPlatform extends PlatformInterface {
throw UnimplementedError('messagesStream() has not been implemented.'); throw UnimplementedError('messagesStream() has not been implemented.');
} }
Stream get resourcesStream {
throw UnimplementedError('resourcesStream() has not been implemented.');
}
Future<bool> initialize([String? deviceName]) { Future<bool> initialize([String? deviceName]) {
throw UnimplementedError('initialize() has not been implemented.'); throw UnimplementedError('initialize() has not been implemented.');
} }
@@ -7,10 +7,14 @@ import 'package:nearby_service/nearby_service.dart';
/// An implementation of [NearbyServiceIOSPlatform] that uses method channels. /// An implementation of [NearbyServiceIOSPlatform] that uses method channels.
class MethodChannelIOSNearbyService extends NearbyServiceIOSPlatform { class MethodChannelIOSNearbyService extends NearbyServiceIOSPlatform {
final messageReceiver = StreamController.broadcast(); final messageReceiver = StreamController.broadcast();
final resourcesReceiver = StreamController.broadcast();
@override @override
Stream get messagesStream => messageReceiver.stream; Stream get messagesStream => messageReceiver.stream;
@override
Stream get resourcesStream => resourcesReceiver.stream;
/// The method channel used to interact with the native platform. /// The method channel used to interact with the native platform.
@visibleForTesting @visibleForTesting
final methodChannel = const MethodChannel('nearby_service'); final methodChannel = const MethodChannel('nearby_service');
@@ -22,6 +26,9 @@ class MethodChannelIOSNearbyService extends NearbyServiceIOSPlatform {
case 'invoke_nearby_service_message_received': case 'invoke_nearby_service_message_received':
messageReceiver.add(handler.arguments); messageReceiver.add(handler.arguments);
break; break;
case 'invoke_nearby_service_resources_received':
resourcesReceiver.add(handler.arguments);
break;
} }
}); });
return (await methodChannel.invokeMethod<bool>( return (await methodChannel.invokeMethod<bool>(
+23 -4
View File
@@ -1,8 +1,8 @@
import 'dart:io';
import 'package:nearby_service/nearby_service.dart'; import 'package:nearby_service/nearby_service.dart';
import 'package:nearby_service/src/utils/json_decoder.dart'; import 'package:nearby_service/src/utils/json_decoder.dart';
import 'logger.dart';
abstract class MessagesStreamMapper { abstract class MessagesStreamMapper {
static ReceivedNearbyMessage replaceId( static ReceivedNearbyMessage replaceId(
ReceivedNearbyMessage message, ReceivedNearbyMessage message,
@@ -22,10 +22,29 @@ abstract class MessagesStreamMapper {
final decoded = JSONDecoder.decodeMap(event); final decoded = JSONDecoder.decodeMap(event);
return ReceivedNearbyMessage.fromJson(decoded); return ReceivedNearbyMessage.fromJson(decoded);
} catch (e) { } catch (e) {
Logger.debug( throw NearbyServiceException(
'Can\'t convert $event to ReceivedNearbyMessage',
);
}
}
}
abstract class ResourcesStreamMapper {
static List<NearbyFile>? toFiles(dynamic event) {
try {
final decoded = JSONDecoder.decodeList(event);
final infoList = [
...?decoded?.map(
(e) => NearbyFileInfo.fromJson(e as Map<String, dynamic>),
)
];
return [
...infoList.map((e) => NearbyFile(info: e, file: File(e.path))),
];
} catch (e) {
throw NearbyServiceException(
'Can\'t convert $event to ReceivedNearbyMessage', 'Can\'t convert $event to ReceivedNearbyMessage',
); );
} }
return null;
} }
} }