feat(android) multiple files support

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