diff --git a/example/lib/main.dart b/example/lib/main.dart index 4a40d36..3ce1969 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -479,15 +479,15 @@ class AppService extends ChangeNotifier { } void sendFilesResponse( - NearbyMessageFilesRequest request, { + String requestId, { required bool response, }) { if (connectedDevice == null) return; _nearbyService.send( OutgoingNearbyMessage( receiver: connectedDevice!.info, - content: NearbyMessageFilesResponse.fromRequest( - request, + content: NearbyMessageFilesResponse( + id: requestId, response: response, ), ), @@ -869,7 +869,7 @@ class _ConnectedBody extends StatelessWidget { ).then((value) { if (value is bool) { context.read().sendFilesResponse( - content, + content.id, response: value, ); } diff --git a/ios/Classes/Models/NearbyMessageContent.swift b/ios/Classes/Models/NearbyMessageContent.swift index 97543f8..99058a6 100644 --- a/ios/Classes/Models/NearbyMessageContent.swift +++ b/ios/Classes/Models/NearbyMessageContent.swift @@ -58,57 +58,64 @@ class NearbyMessageTextContent : NearbyMessageContent { class NearbyMessageFilesContent : NearbyMessageContent { - init(files: Array, id: String, type: MessageContentType) { - self.files = files + init(id: String, type: MessageContentType) { 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) - } - } + if let id: String = json["id"] as? String{ + return NearbyMessageFilesContent(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) + self.files = files + super.init(id: id, type: MessageContentType.filesRequest) } - static func fromJson( json: [String: Any]) -> NearbyMessageFilesRequest? { + 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) + if let filesObjects: Array = json["files"] as? Array> { + let files : [String]? = filesObjects.map({ $0["path"] as? String }).compactMap({$0}) + if let requireMessage = message { + if let requireFiles = files { + return NearbyMessageFilesRequest(files: requireFiles, id: requireMessage.id) + } + } } - return nil + return nil; + } + let files: Array + + override func toJson() -> [String : Any] { + return [ + "files": files.map{["path": $0]}, + ].merging( super.toJson()) { (current, _) in current} } } + class NearbyMessageFilesResponse : NearbyMessageFilesContent { - init(files: Array, id: String, response: Bool) { + init(id: String, response: Bool) { self.response = response - super.init(files: files, id: id, type: MessageContentType.filesResponse) + super.init(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 NearbyMessageFilesResponse(id: requireMessage.id, response: response) } return nil } diff --git a/ios/Classes/NearbyManager.swift b/ios/Classes/NearbyManager.swift index 4fdfcca..fe8b1d7 100644 --- a/ios/Classes/NearbyManager.swift +++ b/ios/Classes/NearbyManager.swift @@ -107,6 +107,10 @@ class NearbyManager: NSObject { 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], diff --git a/ios/Classes/NearbyNotificationReceiver.swift b/ios/Classes/NearbyNotificationReceiver.swift index 0a63ddd..ee11241 100644 --- a/ios/Classes/NearbyNotificationReceiver.swift +++ b/ios/Classes/NearbyNotificationReceiver.swift @@ -9,18 +9,21 @@ 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) { + let cachedRequest = NearbyRequestsStore.instance.find(for: response.id) + if (response.response && cachedRequest != nil) { self.manager.sendFiles( - id: response.id, - paths: response.files, + 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()) @@ -33,7 +36,6 @@ extension NearbyServicePlugin { } @objc func onResourceReceived(notification: Notification) { - DispatchQueue.main.async { if let userInfo = NearbyUserInfo.fromDictionary(userInfo: notification.userInfo) { diff --git a/ios/Classes/Store/NearbyRequestsStore.swift b/ios/Classes/Store/NearbyRequestsStore.swift new file mode 100644 index 0000000..8fc58c9 --- /dev/null +++ b/ios/Classes/Store/NearbyRequestsStore.swift @@ -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} + } +} diff --git a/lib/src/model/nearby_message_content.dart b/lib/src/model/nearby_message_content.dart index 13dd73a..9d00964 100644 --- a/lib/src/model/nearby_message_content.dart +++ b/lib/src/model/nearby_message_content.dart @@ -64,15 +64,7 @@ sealed class NearbyMessageFilesContent extends NearbyMessageContentBase { /// Also [NearbyMessageFilesContent] contains [id] of the files pack and /// list of [NearbyFileInfo] to determine the files. /// - const NearbyMessageFilesContent({ - required this.id, - required this.files, - }); - - /// - /// Info about the files to be sent or received. - /// - final List files; + const NearbyMessageFilesContent({required this.id}); /// /// ID of this files pack @@ -83,51 +75,43 @@ sealed class NearbyMessageFilesContent extends NearbyMessageContentBase { Map toJson() { return { 'id': id, - 'files': [ - ...files.map((e) => e.toJson()), - ], ...super.toJson(), }; } - @override - bool get isValid => - files.isNotEmpty && - files.every( - (element) => element.path.isNotEmpty, - ); - @override bool operator ==(Object other) => identical(this, other) || other is NearbyMessageFilesContent && runtimeType == other.runtimeType && - files == other.files && id == other.id; @override - int get hashCode => files.hashCode ^ id.hashCode; + int get hashCode => id.hashCode; @override String toString() { - return 'NearbyMessageFilesContent{files: $files, id: $id}'; + return 'NearbyMessageFilesContent{id: $id}'; } + + @override + bool get isValid => id.isNotEmpty; } /// -/// Nearby message File Request. Used for file sending requests. -/// Does not contain files' bytes! +/// Nearby message File Request. Used for files sending requests. +/// Contains info about the [files]. /// final class NearbyMessageFilesRequest extends NearbyMessageFilesContent { const NearbyMessageFilesRequest._({ required super.id, - required super.files, + required this.files, }); /// /// Basic constructor with [files] to be sent or received. /// - NearbyMessageFilesRequest({required super.files}) + NearbyMessageFilesRequest({required this.files}) : super( id: RandomUtils.instance.nextInt(1000000, 9999999).toString(), ); @@ -146,6 +130,11 @@ final class NearbyMessageFilesRequest extends NearbyMessageFilesContent { ); } + /// + /// Info about the files to be sent or received. + /// + final List files; + @override NearbyMessageContentType get type => NearbyMessageContentType.filesRequest; @@ -153,6 +142,35 @@ final class NearbyMessageFilesRequest extends NearbyMessageFilesContent { String toString() { return 'NearbyMessageFileRequest{id: $id, files: $files}'; } + + @override + Map toJson() { + return { + 'files': [ + ...files.map((e) => e.toJson()), + ], + ...super.toJson(), + }; + } + + @override + bool get isValid => + super.isValid && + files.isNotEmpty && + files.every( + (element) => element.path.isNotEmpty, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + super == other && + other is NearbyMessageFilesRequest && + runtimeType == other.runtimeType && + files == other.files; + + @override + int get hashCode => super.hashCode ^ files.hashCode; } /// @@ -162,41 +180,19 @@ final class NearbyMessageFilesRequest extends NearbyMessageFilesContent { final class NearbyMessageFilesResponse extends NearbyMessageFilesContent { /// /// Used to send a response to a previously received request. - /// Provide [id] and [files] from [NearbyMessageFilesRequest] or - /// Use the [NearbyMessageFilesResponse.fromRequest] factory to generate a - /// response. + /// Provide [id] from [NearbyMessageFilesRequest] or /// NearbyMessageFilesResponse({ required super.id, - required super.files, required this.response, }); - /// - /// Factory to quickly create a response to [NearbyMessageFilesRequest]. - /// - factory NearbyMessageFilesResponse.fromRequest( - NearbyMessageFilesRequest request, { - required bool response, - }) { - return NearbyMessageFilesResponse( - id: request.id, - files: request.files, - response: response, - ); - } - /// /// Gets [NearbyMessageFilesResponse] from [Map] /// factory NearbyMessageFilesResponse.fromJson(Map? json) { return NearbyMessageFilesResponse( id: json?['id'] ?? '', - files: [ - ...?(json?['files'] as List?)?.map( - (e) => NearbyFileInfo.fromJson(e), - ), - ], response: json?['response'] ?? false, ); } @@ -219,6 +215,6 @@ final class NearbyMessageFilesResponse extends NearbyMessageFilesContent { @override String toString() { - return 'NearbyMessageFileResponse{response: $response, id: $id, files: $files}'; + return 'NearbyMessageFileResponse{response: $response, id: $id}'; } } diff --git a/lib/src/platforms/android/socket_service/file_sockets_manager.dart b/lib/src/platforms/android/socket_service/file_sockets_manager.dart index aef7780..fc1599b 100644 --- a/lib/src/platforms/android/socket_service/file_sockets_manager.dart +++ b/lib/src/platforms/android/socket_service/file_sockets_manager.dart @@ -12,15 +12,23 @@ class FileSocketsManager { NearbyServiceFilesListener? _filesListener; NearbyDeviceInfo? _sender; + NearbyMessageFilesRequest? _cachedFilesRequest; + Future? _socketCreationFuture; + var _connectionData = const NearbyAndroidCommunicationChannelData(); void setListener(NearbyServiceFilesListener? listener) { _filesListener = listener; } + void setConnectionData(NearbyAndroidCommunicationChannelData? data) { + _connectionData = data ?? _connectionData; + } + void onWsRequest(HttpRequest request) { final type = NearbySocketType.fromRequest(request); final filesPackId = NearbyFilesPackId.fromRequest(request); if (type == NearbySocketType.file && filesPackId != null) { + Logger.debug('Save a connection request $filesPackId from client'); _serverWaitingRequests[filesPackId] = request; } } @@ -28,42 +36,62 @@ class FileSocketsManager { Future handleFileMessageContent( NearbyMessageFilesContent content, { required NearbyDeviceInfo? sender, - required NearbyAndroidCommunicationChannelData androidData, required bool isReceived, }) async { if (sender != null) { _sender = sender; Logger.debug('Sender was set to $_sender'); } + final socketExists = _filesSockets[content.id] != null; - final shouldStartSocket = content.byType( - onFilesResponse: (response) => response.response, - onFilesRequest: (_) => true, - ) ?? - false; + final isRequest = content is NearbyMessageFilesRequest; - final alreadyExists = _filesSockets[content.id] != null; + final isPositiveResponse = + content is NearbyMessageFilesResponse && content.response; - if (shouldStartSocket && !alreadyExists) { + if (isRequest) { + _cachedFilesRequest = content; + Logger.debug('Files pack request ${content.id} was cached'); + } + if (!socketExists) { final info = await _service.getConnectionInfo(); if (info != null && info.groupFormed) { if (info.isGroupOwner) { - await _startFilesServer(content); - if (isReceived && content is NearbyMessageFilesResponse) { - await _startDataTransfer(content); + if (isPositiveResponse && _cachedFilesRequest != null) { + _socketCreationFuture = _startFilesServerSocket( + _cachedFilesRequest!, + ); } } else { - await _connectToFilesSocket( - content, - connectionData: androidData, - ownerIpAddress: info.ownerIpAddress, - ); - if (!isReceived && content is NearbyMessageFilesRequest) { - await _startDataTransfer(content); + NearbyMessageFilesRequest? request; + + if (!isReceived && + isPositiveResponse && + _cachedFilesRequest != null) { + request = _cachedFilesRequest!; + } else if (isRequest) { + request = content; + } + + if (request != null) { + _socketCreationFuture = _connectToFilesSocket( + request, + ownerIpAddress: info.ownerIpAddress, + ); } } } } + + if (isReceived && isPositiveResponse && _cachedFilesRequest != null) { + await _socketCreationFuture?.whenComplete( + () async { + await _startDataTransfer(_cachedFilesRequest!); + _socketCreationFuture = null; + _cachedFilesRequest = null; + }, + ); + } } Future closeAll() async { @@ -72,39 +100,39 @@ class FileSocketsManager { } _filesSockets.clear(); _filesListener = null; + _cachedFilesRequest = null; + _socketCreationFuture = null; } Future _connectToFilesSocket( - NearbyMessageFilesContent content, { - required NearbyAndroidCommunicationChannelData connectionData, + NearbyMessageFilesRequest filesRequest, { required String ownerIpAddress, }) async { try { final response = await _network.pingServer( address: ownerIpAddress, - port: connectionData.port, + port: _connectionData.port, ); if (await _pingManager.checkPong(response)) { - await _tryStartFileSocket( - content, + await _startFilesSocket( + filesRequest, onCreateSocket: () => _network.connectToSocket( ownerIpAddress: ownerIpAddress, - port: connectionData.port, + port: _connectionData.port, socketType: NearbySocketType.file, headers: { - NearbyFilesPackId.key: content.id, + NearbyFilesPackId.key: filesRequest.id, }, ), ); } else { Logger.debug( - 'Files server is unavailable, reconnect in ${connectionData.clientReconnectInterval}s', + 'Files server is unavailable, reconnect in ${_connectionData.clientReconnectInterval}s', ); await Future.delayed( - connectionData.clientReconnectInterval, + _connectionData.clientReconnectInterval, () => _connectToFilesSocket( - content, - connectionData: connectionData, + filesRequest, ownerIpAddress: ownerIpAddress, ), ); @@ -114,50 +142,52 @@ class FileSocketsManager { } } - Future _startFilesServer(NearbyMessageFilesContent content) async { - final request = _serverWaitingRequests[content.id]; + Future _startFilesServerSocket( + NearbyMessageFilesRequest filesRequest, + ) async { + final connectionRequest = _serverWaitingRequests[filesRequest.id]; - if (request != null) { - Logger.debug('Found cached server file request ${content.id}'); + if (connectionRequest != null) { + Logger.debug('Found cached server files request ${filesRequest.id}'); - final result = await _tryStartFileSocket( - content, - onCreateSocket: () => WebSocketTransformer.upgrade(request), + final result = await _startFilesSocket( + filesRequest, + onCreateSocket: () => WebSocketTransformer.upgrade(connectionRequest), ); if (result) { - _serverWaitingRequests.remove(content.id); + _serverWaitingRequests.remove(filesRequest.id); } } } - Future _tryStartFileSocket( - NearbyMessageFilesContent content, { + Future _startFilesSocket( + NearbyMessageFilesRequest filesRequest, { required Future Function() onCreateSocket, }) async { final socket = await onCreateSocket(); if (socket != null && _sender != null) { - _filesSockets[content.id] = FilesSocket.startListening( + _filesSockets[filesRequest.id] = FilesSocket.startListening( sender: _sender!, - content: content, + filesRequest: filesRequest, socket: socket, listener: _filesListener, onDestroy: _filesSockets.remove, ); - Logger.info('Created a socket for the files pack ${content.id}'); + Logger.info('Created a socket for the files pack ${filesRequest.id}'); return true; } return false; } - Future _startDataTransfer(NearbyMessageFilesContent content) async { - final filesSocket = _filesSockets[content.id]; + Future _startDataTransfer(NearbyMessageFilesRequest request) async { + final filesSocket = _filesSockets[request.id]; if (filesSocket != null) { - Logger.debug('Start transferring the files pack ${content.id}'); - for (var i = 0; i < content.files.length; i++) { + Logger.debug('Start transferring the files pack ${request.id}'); + for (var i = 0; i < request.files.length; i++) { try { - final fileInfo = content.files[i]; + final fileInfo = request.files[i]; await _streamFile( - content.id, + request.id, filesSocket: filesSocket, file: File(fileInfo.path), )?.asFuture(); @@ -170,7 +200,7 @@ class FileSocketsManager { } } filesSocket.sendData(FilesSocket.finishCommand); - Logger.debug('Sent finish command for the pack ${content.id}'); + Logger.debug('Sent finish command for the pack ${request.id}'); } } 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 0053e5f..b825f64 100644 --- a/lib/src/platforms/android/socket_service/nearby_socket_service.dart +++ b/lib/src/platforms/android/socket_service/nearby_socket_service.dart @@ -58,7 +58,9 @@ class NearbySocketService { _androidData = data.androidData; _connectedDeviceId = data.connectedDeviceId; - _fileSocketsManager.setListener(data.filesListener); + _fileSocketsManager + ..setListener(data.filesListener) + ..setConnectionData(data.androidData); final info = await _service.getConnectionInfo(); @@ -239,7 +241,6 @@ class NearbySocketService { message.content as NearbyMessageFilesContent, isReceived: message is ReceivedNearbyMessage, sender: message is ReceivedNearbyMessage ? message.sender : null, - androidData: _androidData, ); } } diff --git a/lib/src/utils/file_socket.dart b/lib/src/utils/file_socket.dart index b59078c..54fd795 100644 --- a/lib/src/utils/file_socket.dart +++ b/lib/src/utils/file_socket.dart @@ -9,7 +9,7 @@ import 'package:path_provider/path_provider.dart'; class FilesSocket { FilesSocket.startListening({ required this.sender, - required this.content, + required this.filesRequest, required this.listener, required this.onDestroy, required WebSocket socket, @@ -29,7 +29,7 @@ class FilesSocket { static String separateCommandOf(int index) => '$separateCommand$index'; - final NearbyMessageFilesContent content; + final NearbyMessageFilesRequest filesRequest; final void Function(String) onDestroy; final NearbyServiceFilesListener? listener; final NearbyDeviceInfo sender; @@ -52,7 +52,7 @@ class FilesSocket { final logStep = min(pow(10, _chunksCount.toString().length - 1), 100); if (_chunksCount % logStep == 0) { Logger.debug( - 'Got $_chunksCount chunks for the file ${content.files[_currentFileIndex].name}', + 'Got $_chunksCount chunks for the file ${filesRequest.files[_currentFileIndex].name}', ); } } @@ -66,7 +66,7 @@ class FilesSocket { _bytesTable['$_currentFileIndex'] = []; } else if (event == finishCommand) { await Future.wait(_futures); - Logger.info('Files pack ${content.id} was created'); + Logger.info('Files pack ${filesRequest.id} was created'); listener?.onData.call( ReceivedNearbyFilesPack( @@ -74,14 +74,14 @@ class FilesSocket { files: _files, ), ); - onDestroy(content.id); + onDestroy(filesRequest.id); } } Future _createFile(int index) async { try { final bytes = _bytesTable['$index']!; - final fileInfo = content.files[index]; + final fileInfo = filesRequest.files[index]; final directory = await getTemporaryDirectory(); final file = File('${directory.path}/${fileInfo.name}'); await file.writeAsBytes(bytes);