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({
ValueChanged<ReceivedNearbyMessage>? listener,
ValueChanged<File>? onFileSaved,
ValueChanged<List<File>>? 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 = <File>[];
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<String> 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<AppService>().sendFileAccept(content);
context.read<AppService>().sendFilesAccept(content);
}
});
},
);
}
void _onFileSaved(BuildContext context, File file) {
void _onFileSaved(BuildContext context, List<File> 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<String> 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<String>(),
];
});
},
),
),
@@ -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')),
],
);
},
+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
/// 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<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 {
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<NearbyFileInfo> 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<String, dynamic> 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<String, dynamic>? 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<String, dynamic>? 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<String, dynamic> 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}';
}
}
@@ -23,7 +23,7 @@ class FileSocketsManager {
}
Future<void> handleFileMessageContent(
NearbyMessageFileContent content, {
NearbyMessageFilesContent content, {
required NearbyAndroidCommunicationChannelData androidData,
required bool isReceived,
}) async {
@@ -56,7 +56,7 @@ class FileSocketsManager {
}
Future<void> _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<void> _handleServerFileContent(
NearbyMessageFileContent content,
NearbyMessageFilesContent content,
) async {
MapEntry<String, HttpRequest>? 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<void> _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<void> _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 {
@@ -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,
);
@@ -46,7 +46,7 @@ class NearbyServiceMessagesListener
/// Stream Subscription Listener.
///
class NearbyServiceFilesListener
extends NearbyServiceSocketListener<NearbyFile> {
extends NearbyServiceSocketListener<List<NearbyFile>> {
///
/// It is required to pass the [onData] parameter to process the
/// data that came through the stream.
+39 -28
View File
@@ -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<int>) {
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 = <int>[];
final _files = <NearbyFile>[];
final _bytesTable = <String, List<int>>{'0': []};
final _futures = <Future>[];
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<int> 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<int> 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<NearbyFile> getFile() async {
Future<void> _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;
}
}