chore(android) clean up android files sending

This commit is contained in:
ksenia312
2024-02-03 20:05:19 +01:00
parent 695067fc4e
commit 32a9f2a923
8 changed files with 283 additions and 214 deletions
+31 -11
View File
@@ -395,12 +395,17 @@ class AppService extends ChangeNotifier {
final filesListener = NearbyServiceFilesListener(
onData: (event) async {
final files = <File>[];
final directory = Directory(
'storage/emulated/0/Download',
);
for (final nearbyFile in event) {
final content = nearbyFile.info;
final downloadsDir = Directory('storage/emulated/0/Download');
final newFile = await nearbyFile.file.copy(
'${downloadsDir.path}/${DateTime.now().microsecondsSinceEpoch}.${content.extension}',
'${directory.path}/${DateTime.now().microsecondsSinceEpoch}.${nearbyFile.info.extension}',
);
if (!await newFile.exists()) {
await newFile.create();
}
files.add(newFile);
}
onFilesSaved?.call(files);
@@ -436,22 +441,27 @@ class AppService extends ChangeNotifier {
if (connectedDevice == null) return;
_nearbyService.send(
OutgoingNearbyMessage(
content: NearbyMessageFileRequest(
files: [...paths.map((e) => NearbyFileInfo(path: e))],
content: NearbyMessageFilesRequest(
files: [
...paths.map((e) => NearbyFileInfo(path: e)),
],
),
receiver: connectedDevice!.info,
),
);
}
void sendFilesAccept(NearbyMessageFileRequest request) {
void sendFilesResponse(
NearbyMessageFilesRequest request, {
required bool response,
}) {
if (connectedDevice == null) return;
_nearbyService.send(
OutgoingNearbyMessage(
receiver: connectedDevice!.info,
content: NearbyMessageFileResponse.fromRequest(
content: NearbyMessageFilesResponse.fromRequest(
request,
response: true,
response: response,
),
),
);
@@ -827,17 +837,27 @@ class _ConnectedBody extends StatelessWidget {
subtitle: senderSubtitle,
);
},
onFileRequest: (content) {
onFilesRequest: (content) {
ActionDialog.show(
context,
title: 'Files request. Files count: ${content.files.length}',
subtitle: senderSubtitle,
).then((value) {
if (value == true) {
context.read<AppService>().sendFilesAccept(content);
if (value is bool) {
context.read<AppService>().sendFilesResponse(
content,
response: value,
);
}
});
},
onFilesResponse: (content) {
AppShackBar.show(
Scaffold.of(context).context,
content.response ? 'Request is accepted!' : 'Request was denied :(',
subtitle: senderSubtitle,
);
},
);
}
+20 -4
View File
@@ -6,7 +6,9 @@ import 'package:nearby_service/nearby_service.dart';
/// communication channel.
///
/// From the communication channel, you usually get
/// the [NearbyMessageFileRequest] request first. After that, you get [NearbyFile].
/// the [NearbyMessageFilesRequest] request first.
/// After that, you can send positive [NearbyMessageFilesResponse] and
/// get the list of [NearbyFile].
///
class NearbyFile {
///
@@ -44,8 +46,14 @@ class NearbyFile {
}
}
///
/// Quick info about the file
///
class NearbyFileInfo {
const NearbyFileInfo({required this.path, this.sizeBytes});
///
/// Contains the file [path] to get the file from it.
///
const NearbyFileInfo({required this.path});
factory NearbyFileInfo.fromJson(Map<String, dynamic>? json) {
return NearbyFileInfo(
@@ -53,8 +61,10 @@ class NearbyFileInfo {
);
}
///
/// Path of the representing file
///
final String path;
final int? sizeBytes;
@override
bool operator ==(Object other) =>
@@ -72,14 +82,20 @@ class NearbyFileInfo {
};
}
///
/// Quick access to the file [name]
///
String get name {
try {
return path.split('/').last;
} catch (e) {
throw NearbyServiceException('Can\'t get fileName from $path');
return path;
}
}
///
/// Quick access to the file [extension]
///
String get extension {
try {
return name.split('.').last;
+91 -53
View File
@@ -5,13 +5,14 @@ import 'package:nearby_service/src/utils/random.dart';
/// Type of the message.
///
/// If [text], it will be a text message.
/// If [fileRequest], it will be a file request. After accepting request,
/// user can get file stream from connected device.
/// If [filesRequest], it will be a files pack request.
/// After accepting the request,
/// user can get files bytes stream from connected device.
///
enum NearbyMessageContentType {
text,
fileRequest,
fileResponse;
filesRequest,
filesResponse;
///
/// Checks if this is [NearbyMessageContentType.text]
@@ -21,17 +22,17 @@ enum NearbyMessageContentType {
}
///
/// Checks if this is [NearbyMessageContentType.fileRequest]
/// Checks if this is [NearbyMessageContentType.filesRequest]
///
bool get isFileRequest {
return this == NearbyMessageContentType.fileRequest;
bool get isFilesRequest {
return this == NearbyMessageContentType.filesRequest;
}
///
/// Checks if this is [NearbyMessageContentType.fileResponse]
/// Checks if this is [NearbyMessageContentType.filesResponse]
///
bool get isFileResponse {
return this == NearbyMessageContentType.fileResponse;
bool get isFilesResponse {
return this == NearbyMessageContentType.filesResponse;
}
}
@@ -43,20 +44,21 @@ abstract class NearbyMessageContent {
const NearbyMessageContent(this._type);
///
/// Contains the conditional logic of creating [NearbyMessageFileRequest]
/// or [NearbyMessageTextContent] by `type` field of [json].
/// Contains the conditional logic of creating [NearbyMessageFilesRequest],
/// [NearbyMessageFilesResponse] or [NearbyMessageTextContent]
/// by `type` field of [json].
///
factory NearbyMessageContent.fromJson(Map<String, dynamic>? json) {
try {
final type = NearbyMessageContentType.values.firstWhere(
(e) => e.name == json?['type'],
);
if (type.isFileRequest) {
return NearbyMessageFileRequest.fromJson(json);
} else if (type.isText) {
if (type.isText) {
return NearbyMessageTextContent.fromJson(json);
} else if (type.isFileResponse) {
return NearbyMessageFileResponse.fromJson(json);
} else if (type.isFilesRequest) {
return NearbyMessageFilesRequest.fromJson(json);
} else if (type.isFilesResponse) {
return NearbyMessageFilesResponse.fromJson(json);
} else {
throw NearbyServiceException.unsupportedDecoding(json);
}
@@ -76,23 +78,23 @@ abstract class NearbyMessageContent {
/// * The [onText] callback returns this instance of [NearbyMessageContent],
/// cast as [NearbyMessageTextContent] if is a text.
///
/// * The [onFileRequest] callback returns this instance of [NearbyMessageContent],
/// cast as [NearbyMessageFileRequest] if is a file request.
/// * The [onFilesRequest] callback returns this instance of [NearbyMessageContent],
/// cast as [NearbyMessageFilesRequest] if is a files pack request.
///
/// * The [onFileResponse] callback returns this instance of [NearbyMessageContent],
/// cast as [NearbyMessageFileResponse] if is a file response.
/// * The [onFilesResponse] callback returns this instance of [NearbyMessageContent],
/// cast as [NearbyMessageFilesResponse] if is a files pack response.
///
T? byType<T>({
T Function(NearbyMessageTextContent)? onText,
T Function(NearbyMessageFileRequest)? onFileRequest,
T Function(NearbyMessageFileResponse)? onFileResponse,
T Function(NearbyMessageFilesRequest)? onFilesRequest,
T Function(NearbyMessageFilesResponse)? onFilesResponse,
}) {
if (this is NearbyMessageTextContent && onText != null) {
return onText(this as NearbyMessageTextContent);
} else if (this is NearbyMessageFileRequest && onFileRequest != null) {
return onFileRequest(this as NearbyMessageFileRequest);
} else if (this is NearbyMessageFileResponse && onFileResponse != null) {
return onFileResponse(this as NearbyMessageFileResponse);
} else if (this is NearbyMessageFilesRequest && onFilesRequest != null) {
return onFilesRequest(this as NearbyMessageFilesRequest);
} else if (this is NearbyMessageFilesResponse && onFilesResponse != null) {
return onFilesResponse(this as NearbyMessageFilesResponse);
}
return null;
}
@@ -157,15 +159,31 @@ class NearbyMessageTextContent extends NearbyMessageContent {
}
}
abstract class NearbyMessageFilesContent extends NearbyMessageContent {
///
/// Sealed class for files content in NearbyMessage.
///
sealed class NearbyMessageFilesContent extends NearbyMessageContent {
///
/// Here [_type] = [NearbyMessageContentType.filesResponse] or
/// [_type] = [NearbyMessageContentType.filesRequest]
///
/// Also [NearbyMessageFilesContent] contains [id] of the files pack and
/// list of [NearbyFileInfo] to determine the files.
///
const NearbyMessageFilesContent(
super.type, {
required this.id,
required this.files,
});
///
/// Info about the files to be sent or received.
///
final List<NearbyFileInfo> files;
///
/// ID of this files pack
///
final String id;
@override
@@ -179,6 +197,13 @@ abstract class NearbyMessageFilesContent extends NearbyMessageContent {
};
}
@override
bool get isValid =>
files.isNotEmpty &&
files.every(
(element) => element.path.isNotEmpty,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -197,31 +222,31 @@ abstract class NearbyMessageFilesContent extends NearbyMessageContent {
}
///
/// Nearby message File content. Used for file sending requests.
/// Does not contain file bytes!
/// Nearby message File Request. Used for file sending requests.
/// Does not contain files' bytes!
///
class NearbyMessageFileRequest extends NearbyMessageFilesContent {
const NearbyMessageFileRequest._({
class NearbyMessageFilesRequest extends NearbyMessageFilesContent {
const NearbyMessageFilesRequest._({
required super.id,
required super.files,
}) : super(
NearbyMessageContentType.fileRequest,
NearbyMessageContentType.filesRequest,
);
///
/// Basic constructor with [files] to be sent or received.
///
NearbyMessageFileRequest({required super.files})
NearbyMessageFilesRequest({required super.files})
: super(
NearbyMessageContentType.fileRequest,
NearbyMessageContentType.filesRequest,
id: RandomUtils.instance.nextInt(1000000, 9999999).toString(),
);
///
/// Gets [NearbyMessageFileRequest] from [json].
/// Gets [NearbyMessageFilesRequest] from [json].
///
factory NearbyMessageFileRequest.fromJson(Map<String, dynamic>? json) {
return NearbyMessageFileRequest._(
factory NearbyMessageFilesRequest.fromJson(Map<String, dynamic>? json) {
return NearbyMessageFilesRequest._(
id: json?['id'] ?? '',
files: [
...?(json?['files'] as List?)?.map(
@@ -231,37 +256,50 @@ class NearbyMessageFileRequest extends NearbyMessageFilesContent {
);
}
@override
bool get isValid => files.every((element) => element.path.isNotEmpty);
@override
String toString() {
return 'NearbyMessageFileRequest{id: $id, files: $files}';
}
}
class NearbyMessageFileResponse extends NearbyMessageFilesContent {
NearbyMessageFileResponse({
///
/// Nearby message File Response. Used for file sending responses.
/// Does not contain files' bytes!
///
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.
///
NearbyMessageFilesResponse({
required super.id,
required super.files,
required this.response,
}) : super(
NearbyMessageContentType.fileResponse,
NearbyMessageContentType.filesResponse,
);
factory NearbyMessageFileResponse.fromRequest(
NearbyMessageFileRequest request, {
///
/// Factory to quickly create a response to [NearbyMessageFilesRequest].
///
factory NearbyMessageFilesResponse.fromRequest(
NearbyMessageFilesRequest request, {
required bool response,
}) {
return NearbyMessageFileResponse(
return NearbyMessageFilesResponse(
id: request.id,
files: request.files,
response: response,
);
}
factory NearbyMessageFileResponse.fromJson(Map<String, dynamic>? json) {
return NearbyMessageFileResponse(
///
/// Gets [NearbyMessageFilesResponse] from [Map]
///
factory NearbyMessageFilesResponse.fromJson(Map<String, dynamic>? json) {
return NearbyMessageFilesResponse(
id: json?['id'] ?? '',
files: [
...?(json?['files'] as List?)?.map(
@@ -272,11 +310,11 @@ class NearbyMessageFileResponse extends NearbyMessageFilesContent {
);
}
///
/// The main response to the received [NearbyMessageFilesRequest].
///
final bool response;
@override
bool get isValid => files.every((element) => element.path.isNotEmpty);
@override
Map<String, dynamic> toJson() {
return {
@@ -289,4 +327,4 @@ class NearbyMessageFileResponse extends NearbyMessageFilesContent {
String toString() {
return 'NearbyMessageFileResponse{response: $response, id: $id, files: $files}';
}
}
}
@@ -1,12 +1,14 @@
part of 'nearby_socket_service.dart';
class FileSocketsManager {
FileSocketsManager(this._network, this._service);
FileSocketsManager(this._network, this._service, this._pingManager);
final NearbyServiceNetwork _network;
final NearbyAndroidService _service;
final _filesSockets = <FilesSocket>[];
final _serverWaitingContents = <String, HttpRequest>{};
final NearbySocketPingManager _pingManager;
final _filesSockets = <String, FilesSocket>{};
final _serverWaitingRequests = <String, HttpRequest>{};
NearbyServiceFilesListener? _filesListener;
@@ -16,9 +18,9 @@ class FileSocketsManager {
void onWsRequest(HttpRequest request) {
final type = NearbySocketType.fromRequest(request);
final fileId = NearbyFileId.fromRequest(request);
if (type == NearbySocketType.file && fileId != null) {
_serverWaitingContents[fileId] = request;
final filesPackId = NearbyFilesPackId.fromRequest(request);
if (type == NearbySocketType.file && filesPackId != null) {
_serverWaitingRequests[filesPackId] = request;
}
}
@@ -27,114 +29,124 @@ class FileSocketsManager {
required NearbyAndroidCommunicationChannelData androidData,
required bool isReceived,
}) async {
final info = await _service.getConnectionInfo();
if (info != null && info.groupFormed) {
if (info.isGroupOwner) {
await _handleServerFileContent(content);
if (content is NearbyMessageFileResponse && isReceived) {
await _tryTransferData(content);
}
} else {
await _handleClientFileContent(
content,
connectionData: androidData,
ownerIpAddress: info.ownerIpAddress,
);
if (content is NearbyMessageFileRequest && !isReceived) {
await _tryTransferData(content);
final shouldStartSocket = content.byType(
onFilesResponse: (response) => response.response,
onFilesRequest: (_) => true,
) ??
false;
if (shouldStartSocket) {
final info = await _service.getConnectionInfo();
if (info != null && info.groupFormed) {
if (info.isGroupOwner) {
await _startFilesServer(content);
if (content is NearbyMessageFilesResponse && isReceived) {
await _tryTransferData(content);
}
} else {
await _connectToFilesSocket(
content,
connectionData: androidData,
ownerIpAddress: info.ownerIpAddress,
);
if (content is NearbyMessageFilesRequest && !isReceived) {
await _tryTransferData(content);
}
}
}
} else {
_filesSockets.remove(content.id);
}
}
Future<void> closeAll() async {
for (final fileSocket in _filesSockets) {
for (final fileSocket in _filesSockets.values) {
await fileSocket.close();
}
_filesSockets.clear();
_filesListener = null;
}
Future<void> _handleClientFileContent(
Future<void> _connectToFilesSocket(
NearbyMessageFilesContent content, {
required NearbyAndroidCommunicationChannelData connectionData,
required String ownerIpAddress,
}) async {
try {
final shouldStartFileSocket = content.byType(
onFileResponse: (response) => response.response,
onFileRequest: (request) => true,
) ??
false;
if (shouldStartFileSocket) {
final socket = await _network.connectToSocket(
ownerIpAddress: ownerIpAddress,
port: connectionData.port,
socketType: NearbySocketType.file,
headers: {
NearbyFileId.key: content.id,
},
final response = await _network.pingServer(
address: ownerIpAddress,
port: connectionData.port,
);
if (await _pingManager.checkPong(response)) {
await _tryStartFileSocket(
content,
onCreateSocket: () => _network.connectToSocket(
ownerIpAddress: ownerIpAddress,
port: connectionData.port,
socketType: NearbySocketType.file,
headers: {
NearbyFilesPackId.key: content.id,
},
),
);
} else {
Logger.debug(
'Files server is unavailable, reconnect in ${connectionData.clientReconnectInterval}s',
);
await Future.delayed(
connectionData.clientReconnectInterval,
() => _connectToFilesSocket(
content,
connectionData: connectionData,
ownerIpAddress: ownerIpAddress,
),
);
if (socket != null) {
_filesSockets.add(
FilesSocket.startListening(
content: content,
socket: socket,
listener: _filesListener,
onDestroy: _filesSockets.remove,
),
);
Logger.info(
'The file socket was created for the files pack ${content.id}',
);
} else {
await Future.delayed(
connectionData.clientReconnectInterval,
() => _handleClientFileContent(
content,
connectionData: connectionData,
ownerIpAddress: ownerIpAddress,
),
);
}
}
} catch (e) {
Logger.error(e);
}
}
Future<void> _handleServerFileContent(
Future<void> _startFilesServer(
NearbyMessageFilesContent content,
) async {
MapEntry<String, HttpRequest>? request;
try {
request = _serverWaitingContents.entries.firstWhere(
(element) => element.key == content.id,
);
} catch (e) {
request = null;
}
final request = _serverWaitingRequests[content.id];
if (request != null) {
Logger.debug('Found cached server file request ${request.key}');
_filesSockets.add(
FilesSocket.startListening(
content: content,
socket: await WebSocketTransformer.upgrade(request.value),
listener: _filesListener,
onDestroy: _filesSockets.remove,
),
Logger.debug('Found cached server file request ${content.id}');
final result = await _tryStartFileSocket(
content,
onCreateSocket: () => WebSocketTransformer.upgrade(request),
);
_serverWaitingContents.remove(content.id);
Logger.info('Created a socket for the files pack ${content.id}');
if (result) {
_serverWaitingRequests.remove(content.id);
}
}
}
Future<bool> _tryStartFileSocket(
NearbyMessageFilesContent content, {
required Future<WebSocket?> Function() onCreateSocket,
}) async {
final socket = await onCreateSocket();
if (socket != null) {
_filesSockets[content.id] = FilesSocket.startListening(
content: content,
socket: socket,
listener: _filesListener,
onDestroy: _filesSockets.remove,
);
Logger.info('Created a socket for the files pack ${content.id}');
return true;
}
return false;
}
Future<void> _tryTransferData(NearbyMessageFilesContent content) async {
Logger.debug('Start transferring the files pack ${content.id}');
final filesSocket = _find(content.id);
final filesSocket = _filesSockets[content.id];
if (filesSocket != null) {
Logger.debug('Start transferring the files pack ${content.id}');
for (var i = 0; i < content.files.length; i++) {
try {
final fileInfo = content.files[i];
@@ -161,22 +173,6 @@ class FileSocketsManager {
required FilesSocket filesSocket,
required File file,
}) {
return file.openRead().listen(
(data) {
filesSocket.sendData(data);
},
);
}
FilesSocket? _find(String id) {
FilesSocket? fileSocket;
try {
fileSocket = _filesSockets.firstWhere(
(element) => element.content.id == id,
);
} catch (e) {
fileSocket = null;
}
return fileSocket;
return file.openRead().listen(filesSocket.sendData);
}
}
@@ -24,7 +24,11 @@ class NearbySocketService {
final NearbyAndroidService _service;
final _pingManager = NearbySocketPingManager();
final _network = NearbyServiceNetwork();
late final _fileSocketsManager = FileSocketsManager(_network, _service);
late final _fileSocketsManager = FileSocketsManager(
_network,
_service,
_pingManager,
);
final state = ValueNotifier(CommunicationChannelState.notConnected);
@@ -77,7 +81,7 @@ class NearbySocketService {
}
///
/// Add [OutgoingNearbyMessage]'s JSON representation to [_socket].
/// Adds [OutgoingNearbyMessage]'s JSON representation to the [_socket].
///
Future<bool> send(OutgoingNearbyMessage message) async {
if (message.isValid) {
@@ -199,36 +203,22 @@ class NearbySocketService {
Logger.debug('Starting socket subscription');
if (_connectedDeviceId != null) {
_messagesSubscription = _socket?.listen(
(event) async {
// if (fileLoaders != null) {
// if (event is List<int>) {
// fileLoaders!.add(event);
// } else if (event == fileLoaders?.finishCommand) {
// final file = await fileLoaders!.getFile().whenComplete(
// () {
// fileLoaders = null;
// },
// );
// socketListener.onFile?.call(file);
// }
// } else {
_messagesSubscription = _socket
?.map(MessagesStreamMapper.toMessage)
.where((e) => e != null)
.cast<ReceivedNearbyMessage>()
.map((e) => MessagesStreamMapper.replaceId(e, _connectedDeviceId!))
.listen(
(message) async {
try {
final message = MessagesStreamMapper.toMessage(event);
if (message != null) {
final newMessage = MessagesStreamMapper.replaceId(
message,
_connectedDeviceId!,
if (message.content is NearbyMessageFilesContent) {
_fileSocketsManager.handleFileMessageContent(
message.content as NearbyMessageFilesContent,
androidData: _androidData,
isReceived: true,
);
if (newMessage.content is NearbyMessageFilesContent) {
_fileSocketsManager.handleFileMessageContent(
newMessage.content as NearbyMessageFilesContent,
androidData: _androidData,
isReceived: true,
);
}
socketListener.onData(newMessage);
}
socketListener.onData(message);
} catch (e) {
Logger.error(e);
}
@@ -101,10 +101,10 @@ enum NearbySocketType {
}
}
class NearbyFileId {
static const key = 'FileID';
class NearbyFilesPackId {
static const key = 'FilesPackID';
static String? fromRequest(HttpRequest request) {
return request.headers.value(NearbyFileId.key);
return request.headers.value(NearbyFilesPackId.key);
}
}
+5
View File
@@ -38,4 +38,9 @@ class NearbyServiceException implements Exception {
}
final Object? error;
@override
String toString() {
return 'NearbyServiceException{error: $error}';
}
}
+21 -17
View File
@@ -9,25 +9,12 @@ import 'package:path_provider/path_provider.dart';
class FilesSocket {
FilesSocket.startListening({
required this.content,
required this.listener,
required this.onDestroy,
required WebSocket socket,
required NearbyServiceFilesListener? listener,
required void Function(FilesSocket) onDestroy,
}) : _socket = socket {
_socket.listen(
(event) async {
if (event is List<int>) {
addChunk(event);
} else if (event == separateCommandOf(_currentFileIndex)) {
_futures.add(_createFile(_currentFileIndex));
_currentFileIndex = _currentFileIndex + 1;
_bytesTable['$_currentFileIndex'] = [];
} else if (event == finishCommand) {
await Future.wait(_futures);
Logger.info('Files pack ${content.id} was created');
listener?.onData.call(_files);
onDestroy(this);
}
},
_listener,
onError: listener?.onError,
cancelOnError: listener?.cancelOnError,
onDone: listener?.onDone,
@@ -35,13 +22,15 @@ class FilesSocket {
listener?.onCreated?.call();
}
static const finishCommand = '_@@FINISH_SENDING_FILE_';
static const finishCommand = '_@@FINISH_SENDING_FILES_';
static const separateCommand = '_@@SEPARATE_SENDING_FILE_';
static String separateCommandOf(int index) => '$separateCommand$index';
final NearbyMessageFilesContent content;
final void Function(String) onDestroy;
final NearbyServiceFilesListener? listener;
final WebSocket _socket;
final _files = <NearbyFile>[];
@@ -66,6 +55,21 @@ class FilesSocket {
}
}
Future<void> _listener(dynamic event) async {
if (event is List<int>) {
addChunk(event);
} else if (event == separateCommandOf(_currentFileIndex)) {
_futures.add(_createFile(_currentFileIndex));
_currentFileIndex = _currentFileIndex + 1;
_bytesTable['$_currentFileIndex'] = [];
} else if (event == finishCommand) {
await Future.wait(_futures);
Logger.info('Files pack ${content.id} was created');
listener?.onData.call(_files);
onDestroy(content.id);
}
}
Future<void> _createFile(int index) async {
try {
final bytes = _bytesTable['$index']!;