diff --git a/example/lib/main.dart b/example/lib/main.dart index 446e649..53b64a6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -379,7 +379,7 @@ class AppService extends ChangeNotifier { Future startCommunicationChannel({ ValueChanged? listener, - ValueChanged? onFileSaved, + ValueChanged>? onFilesSaved, }) async { final messagesListener = NearbyServiceMessagesListener( onCreated: () { @@ -394,12 +394,16 @@ class AppService extends ChangeNotifier { ); final filesListener = NearbyServiceFilesListener( onData: (event) async { - final content = event.content; - final downloadsDir = Directory('storage/emulated/0/Download'); - final newFile = await event.file.copy( - '${downloadsDir.path}/${content.id}_${content.fileName}', - ); - onFileSaved?.call(newFile); + final files = []; + for (final nearbyFile in event) { + final content = nearbyFile.info; + final downloadsDir = Directory('storage/emulated/0/Download'); + final newFile = await nearbyFile.file.copy( + '${downloadsDir.path}/${content.name}', + ); + files.add(newFile); + } + onFilesSaved?.call(files); }, ); @@ -413,26 +417,33 @@ class AppService extends ChangeNotifier { } void sendMessage(String message) { + try { + if (connectedDevice == null) return; + _nearbyService.send( + OutgoingNearbyMessage( + content: NearbyMessageTextContent(value: message), + receiver: connectedDevice!.info, + ), + ); + } catch (e, s) { + print(e); + print(s); + } + } + + void sendFilesRequest(List paths) { if (connectedDevice == null) return; _nearbyService.send( OutgoingNearbyMessage( - content: NearbyMessageTextContent(value: message), + content: NearbyMessageFileRequest( + files: [...paths.map((e) => NearbyFileInfo(path: e))], + ), receiver: connectedDevice!.info, ), ); } - void sendFileRequest(String filePath) { - if (connectedDevice == null) return; - _nearbyService.send( - OutgoingNearbyMessage( - content: NearbyMessageFileRequest(filePath: filePath), - receiver: connectedDevice!.info, - ), - ); - } - - void sendFileAccept(NearbyMessageFileRequest request) { + void sendFilesAccept(NearbyMessageFileRequest request) { if (connectedDevice == null) return; _nearbyService.send( OutgoingNearbyMessage( @@ -788,7 +799,7 @@ class _ConnectedBody extends StatelessWidget { title: 'Start communicate', onTap: () => service.startCommunicationChannel( listener: (event) => _listener(context, event), - onFileSaved: (file) => _onFileSaved(context, file), + onFilesSaved: (files) => _onFileSaved(context, files), ), ) else @@ -818,21 +829,21 @@ class _ConnectedBody extends StatelessWidget { onFileRequest: (content) { ActionDialog.show( context, - title: 'File request ${content.fileName}', + title: 'Files request. Files count: ${content.files.length}', subtitle: senderSubtitle, ).then((value) { if (value == true) { - context.read().sendFileAccept(content); + context.read().sendFilesAccept(content); } }); }, ); } - void _onFileSaved(BuildContext context, File file) { + void _onFileSaved(BuildContext context, List files) { AppShackBar.show( Scaffold.of(context).context, - 'File saved to ${file.path}', + '${files.length} files saved! \n ${files.map((e) => e.path).join('\n')}', ); } } @@ -846,7 +857,7 @@ class _ConnectedSocketBody extends StatefulWidget { class _ConnectedSocketBodyState extends State<_ConnectedSocketBody> { String message = ''; - String filePath = ''; + List filePaths = []; @override Widget build(BuildContext context) { @@ -905,14 +916,18 @@ class _ConnectedSocketBodyState extends State<_ConnectedSocketBody> { flex: 2, child: _ActionButton( type: _ActionButtonType.warning, - title: 'Choose a file', + title: 'Choose files', onTap: () async { - final result = await FilePicker.platform.pickFiles(); - if (result != null && result.isSinglePick) { - setState(() { - filePath = result.paths.first!; - }); - } + final result = await FilePicker.platform.pickFiles( + allowMultiple: true, + ); + setState(() { + filePaths = [ + ...?result?.paths + .where((e) => e != null) + .cast(), + ]; + }); }, ), ), @@ -921,7 +936,7 @@ class _ConnectedSocketBodyState extends State<_ConnectedSocketBody> { child: _ActionButton( title: 'Send', onTap: () { - service.sendFileRequest(filePath); + service.sendFilesRequest(filePaths); }, ), ), @@ -929,7 +944,8 @@ class _ConnectedSocketBodyState extends State<_ConnectedSocketBody> { ), ), const SizedBox(height: 10), - Text('Selected file: $filePath'), + const Text('Selected files:', style: TextStyle(fontSize: 18)), + Text(filePaths.join('\n')), ], ); }, diff --git a/lib/src/models/nearby_file.dart b/lib/src/models/nearby_file.dart index 895e259..caa264b 100644 --- a/lib/src/models/nearby_file.dart +++ b/lib/src/models/nearby_file.dart @@ -5,24 +5,22 @@ import 'package:nearby_service/nearby_service.dart'; /// A representation of a file that can be got from the Nearby Service's /// communication channel. /// -/// You can use [id] to compare it to the id from [NearbyMessageFileRequest.id]. -/// /// From the communication channel, you usually get /// the [NearbyMessageFileRequest] request first. After that, you get [NearbyFile]. /// class NearbyFile { /// - /// Pass [content] assigned to file to be sent. + /// Pass [info] assigned to file to be sent. /// const NearbyFile({ - required this.content, + required this.info, required this.file, }); /// /// Quick info about the file /// - final NearbyMessageFileContent content; + final NearbyFileInfo info; /// /// A file that you can save in your phone if needed @@ -34,14 +32,56 @@ class NearbyFile { identical(this, other) || other is NearbyFile && runtimeType == other.runtimeType && - content == other.content && + info == other.info && file == other.file; @override - int get hashCode => content.hashCode ^ file.hashCode; + int get hashCode => info.hashCode ^ file.hashCode; @override String toString() { - return 'NearbyFile{content: $content, file: $file}'; + return 'NearbyFile{info: $info, file: $file}'; + } +} + +class NearbyFileInfo { + const NearbyFileInfo({required this.path, this.sizeBytes}); + + factory NearbyFileInfo.fromJson(Map? json) { + return NearbyFileInfo( + path: json?['path'] ?? '', + ); + } + + final String path; + final int? sizeBytes; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is NearbyFileInfo && + runtimeType == other.runtimeType && + path == other.path; + + @override + int get hashCode => path.hashCode; + + Map toJson() { + return { + 'path': path, + }; + } + + String get name { + try { + return path.split('/').last; + } catch (e) { + throw NearbyServiceException('Can\'t get fileName from $path'); + } + } + + @override + String toString() { + return 'NearbyFileInfo{path: $path}'; } } diff --git a/lib/src/models/nearby_message_content.dart b/lib/src/models/nearby_message_content.dart index 202a6fe..38d77d0 100644 --- a/lib/src/models/nearby_message_content.dart +++ b/lib/src/models/nearby_message_content.dart @@ -157,70 +157,61 @@ class NearbyMessageTextContent extends NearbyMessageContent { } } -abstract class NearbyMessageFileContent extends NearbyMessageContent { - const NearbyMessageFileContent( +abstract class NearbyMessageFilesContent extends NearbyMessageContent { + const NearbyMessageFilesContent( super.type, { required this.id, - required this.filePath, + required this.files, }); - /// - /// ID for comparing file requests. - /// + final List files; + final String id; - /// - /// The name of the file to be sent or received. - /// - final String filePath; - - String get fileName { - try { - return filePath.split('/').last; - } catch (e) { - throw NearbyServiceException('Can\'t get fileName from $filePath'); - } - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is NearbyMessageFileContent && - runtimeType == other.runtimeType && - id == other.id && - filePath == other.filePath; - - @override - int get hashCode => id.hashCode ^ filePath.hashCode; - @override Map toJson() { return { 'id': id, - 'filePath': filePath, + 'files': [ + ...files.map((e) => e.toJson()), + ], ...super.toJson(), }; } + + @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; + + @override + String toString() { + return 'NearbyMessageFilesContent{files: $files, id: $id}'; + } } /// /// Nearby message File content. Used for file sending requests. /// Does not contain file bytes! /// -/// Contains [filePath] - the name of file to be sent or received. -/// -class NearbyMessageFileRequest extends NearbyMessageFileContent { +class NearbyMessageFileRequest extends NearbyMessageFilesContent { const NearbyMessageFileRequest._({ required super.id, - required super.filePath, + required super.files, }) : super( NearbyMessageContentType.fileRequest, ); /// - /// Basic constructor with [filePath] to be sent or received. + /// Basic constructor with [files] to be sent or received. /// - NearbyMessageFileRequest({required super.filePath}) + NearbyMessageFileRequest({required super.files}) : super( NearbyMessageContentType.fileRequest, id: RandomUtils.instance.nextInt(1000000, 9999999).toString(), @@ -232,23 +223,27 @@ class NearbyMessageFileRequest extends NearbyMessageFileContent { factory NearbyMessageFileRequest.fromJson(Map? json) { return NearbyMessageFileRequest._( id: json?['id'] ?? '', - filePath: json?['filePath'] ?? '', + files: [ + ...?(json?['files'] as List?)?.map( + (e) => NearbyFileInfo.fromJson(e), + ), + ], ); } @override - bool get isValid => filePath.isNotEmpty; + bool get isValid => files.every((element) => element.path.isNotEmpty); @override String toString() { - return 'NearbyMessageFileRequest{id: $id, filePath: $filePath}'; + return 'NearbyMessageFileRequest{id: $id, files: $files}'; } } -class NearbyMessageFileResponse extends NearbyMessageFileContent { +class NearbyMessageFileResponse extends NearbyMessageFilesContent { NearbyMessageFileResponse({ required super.id, - required super.filePath, + required super.files, required this.response, }) : super( NearbyMessageContentType.fileResponse, @@ -260,7 +255,7 @@ class NearbyMessageFileResponse extends NearbyMessageFileContent { }) { return NearbyMessageFileResponse( id: request.id, - filePath: request.filePath, + files: request.files, response: response, ); } @@ -268,7 +263,11 @@ class NearbyMessageFileResponse extends NearbyMessageFileContent { factory NearbyMessageFileResponse.fromJson(Map? json) { return NearbyMessageFileResponse( id: json?['id'] ?? '', - filePath: json?['filePath'] ?? '', + files: [ + ...?(json?['files'] as List?)?.map( + (e) => NearbyFileInfo.fromJson(e), + ), + ], response: json?['response'] ?? false, ); } @@ -276,7 +275,7 @@ class NearbyMessageFileResponse extends NearbyMessageFileContent { final bool response; @override - bool get isValid => filePath.isNotEmpty; + bool get isValid => files.every((element) => element.path.isNotEmpty); @override Map toJson() { @@ -288,6 +287,6 @@ class NearbyMessageFileResponse extends NearbyMessageFileContent { @override String toString() { - return 'NearbyMessageFileResponse{response: $response, id: $id, filePath: $filePath}'; + return 'NearbyMessageFileResponse{response: $response, id: $id, files: $files}'; } } 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 7679b26..260e0a8 100644 --- a/lib/src/platforms/android/socket_service/file_sockets_manager.dart +++ b/lib/src/platforms/android/socket_service/file_sockets_manager.dart @@ -23,7 +23,7 @@ class FileSocketsManager { } Future handleFileMessageContent( - NearbyMessageFileContent content, { + NearbyMessageFilesContent content, { required NearbyAndroidCommunicationChannelData androidData, required bool isReceived, }) async { @@ -56,7 +56,7 @@ class FileSocketsManager { } Future _handleClientFileContent( - NearbyMessageFileContent content, { + NearbyMessageFilesContent content, { required NearbyAndroidCommunicationChannelData connectionData, required String ownerIpAddress, }) async { @@ -86,7 +86,7 @@ class FileSocketsManager { ), ); Logger.info( - 'The file socket was created for the file ${content.fileName}', + 'The file socket was created for the files pack ${content.id}', ); } else { await Future.delayed( @@ -105,7 +105,7 @@ class FileSocketsManager { } Future _handleServerFileContent( - NearbyMessageFileContent content, + NearbyMessageFilesContent content, ) async { MapEntry? request; try { @@ -127,30 +127,47 @@ class FileSocketsManager { ), ); _serverWaitingContents.remove(content.id); - Logger.info('Created file socket for ${content.fileName}'); + Logger.info('Created a socket for the files pack ${content.id}'); } } - Future _tryTransferData(NearbyMessageFileContent content) async { - final fileSocket = _find(content.id); - if (fileSocket != null) { - Logger.info('Start transferring a file ${content.fileName}'); - final file = File(content.filePath); + Future _tryTransferData(NearbyMessageFilesContent content) async { + Logger.debug('Start transferring the files pack ${content.id}'); + final filesSocket = _find(content.id); + if (filesSocket != null) { + for (var i = 0; i < content.files.length; i++) { + try { + final fileInfo = content.files[i]; + await _streamFile( + content.id, + filesSocket: filesSocket, + file: File(fileInfo.path), + )?.asFuture(); - file.openRead().listen( - (data) { - fileSocket.sendData(data); - }, - onDone: () { - Logger.debug('Sent finished command for ${content.fileName}'); - fileSocket.sendData( - FilesSocket.generateFinishCommand(fileSocket.content.id), - ); - }, - ); + filesSocket.sendData(FilesSocket.separateCommandOf(i)); + Logger.debug('Sent separate command for file №$i'); + } catch (e) { + Logger.error(e); + continue; + } + } + filesSocket.sendData(FilesSocket.finishCommand); + Logger.debug('Sent finish command for the pack ${content.id}'); } } + StreamSubscription? _streamFile( + String id, { + required FilesSocket filesSocket, + required File file, + }) { + return file.openRead().listen( + (data) { + filesSocket.sendData(data); + }, + ); + } + FilesSocket? _find(String id) { FilesSocket? fileSocket; try { 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 1baf244..22ffbb3 100644 --- a/lib/src/platforms/android/socket_service/nearby_socket_service.dart +++ b/lib/src/platforms/android/socket_service/nearby_socket_service.dart @@ -92,9 +92,9 @@ class NearbySocketService { }, ), ); - if (message.content is NearbyMessageFileContent) { + if (message.content is NearbyMessageFilesContent) { _fileSocketsManager.handleFileMessageContent( - message.content as NearbyMessageFileContent, + message.content as NearbyMessageFilesContent, androidData: _androidData, isReceived: false, ); @@ -220,9 +220,9 @@ class NearbySocketService { message, _connectedDeviceId!, ); - if (newMessage.content is NearbyMessageFileContent) { + if (newMessage.content is NearbyMessageFilesContent) { _fileSocketsManager.handleFileMessageContent( - newMessage.content as NearbyMessageFileContent, + newMessage.content as NearbyMessageFilesContent, androidData: _androidData, isReceived: true, ); diff --git a/lib/src/types/nearby_service_stream_listener.dart b/lib/src/types/nearby_service_stream_listener.dart index 0592c57..62a513d 100644 --- a/lib/src/types/nearby_service_stream_listener.dart +++ b/lib/src/types/nearby_service_stream_listener.dart @@ -46,7 +46,7 @@ class NearbyServiceMessagesListener /// Stream Subscription Listener. /// class NearbyServiceFilesListener - extends NearbyServiceSocketListener { + extends NearbyServiceSocketListener> { /// /// It is required to pass the [onData] parameter to process the /// data that came through the stream. diff --git a/lib/src/utils/file_socket.dart b/lib/src/utils/file_socket.dart index cc93468..4243a6e 100644 --- a/lib/src/utils/file_socket.dart +++ b/lib/src/utils/file_socket.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'dart:math'; @@ -15,15 +16,16 @@ class FilesSocket { _socket.listen( (event) async { if (event is List) { - saveChunk(event); + addChunk(event); + } else if (event == separateCommandOf(_currentFileIndex)) { + _futures.add(_createFile(_currentFileIndex)); + _currentFileIndex = _currentFileIndex + 1; + _bytesTable['$_currentFileIndex'] = []; } else if (event == finishCommand) { - final file = await getFile().whenComplete( - () { - onDestroy(this); - }, - ); - Logger.info('File ${file.content.fileName} was created'); - listener?.onData.call(file); + await Future.wait(_futures); + Logger.info('Files pack ${content.id} was created'); + listener?.onData.call(_files); + onDestroy(this); } }, onError: listener?.onError, @@ -33,43 +35,52 @@ class FilesSocket { listener?.onCreated?.call(); } - static const _finishCommand = '@@FINISH_SENDING_FILE_'; + static const finishCommand = '_@@FINISH_SENDING_FILE_'; - static String generateFinishCommand(String id) { - return '$_finishCommand$id'; - } + static const separateCommand = '_@@SEPARATE_SENDING_FILE_'; + + static String separateCommandOf(int index) => '$separateCommand$index'; + + final NearbyMessageFilesContent content; final WebSocket _socket; - final NearbyMessageFileContent content; - final _bytes = []; + final _files = []; + final _bytesTable = >{'0': []}; + final _futures = []; - int chunksCount = 0; - - String get finishCommand => '$_finishCommand${content.id}'; + int _chunksCount = 0; + int _currentFileIndex = 0; void sendData(dynamic event) { _socket.add(event); } - void saveChunk(List value) { - _bytes.addAll(value); - chunksCount = chunksCount + 1; - final logStep = min(pow(10, chunksCount.toString().length - 1), 100); - if (chunksCount % logStep == 0) { - Logger.debug('Got $chunksCount chunks for the file ${content.fileName}'); + void addChunk(List value) { + _bytesTable['$_currentFileIndex']?.addAll(value); + _chunksCount = _chunksCount + 1; + 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}', + ); } } - Future getFile() async { + Future _createFile(int index) async { try { + final bytes = _bytesTable['$index']!; + final fileInfo = content.files[index]; final directory = await getTemporaryDirectory(); - final file = File('${directory.path}/${content.fileName}'); + final file = File('${directory.path}/${fileInfo.name}'); - await file.writeAsBytes(_bytes); - return NearbyFile(file: file, content: content); + await file.writeAsBytes(bytes); + + final nearbyFile = NearbyFile(file: file, info: fileInfo); + _files.add(nearbyFile); + + Logger.info('File ${nearbyFile.info.name} was created'); } catch (e) { Logger.error(e); - rethrow; } }