diff --git a/example/lib/main.dart b/example/lib/main.dart index 8f97c3b..446e649 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -193,6 +193,7 @@ class AppService extends ChangeNotifier { List? peers; NearbyDevice? connectedDevice; NearbyDeviceInfo? currentDeviceInfo; + NearbyConnectionAndroidInfo? connectionAndroidInfo; String platformVersion = 'Unknown'; String platformModel = 'Unknown'; @@ -217,8 +218,7 @@ class AppService extends ChangeNotifier { } bool get isAndroidGroupOwner { - return Platform.isAndroid && - (_nearbyService.android?.connectionInfo?.isGroupOwner ?? false); + return Platform.isAndroid && (connectionAndroidInfo?.isGroupOwner ?? false); } Future getPlatformInfo() async { @@ -332,6 +332,7 @@ class AppService extends ChangeNotifier { Future connect(NearbyDevice device) async { try { await _nearbyService.connect(device); + connectionAndroidInfo = await _nearbyService.android?.getConnectionInfo(); } catch (e) { if (kDebugMode) { print(e); @@ -380,34 +381,33 @@ class AppService extends ChangeNotifier { ValueChanged? listener, ValueChanged? onFileSaved, }) async { - final eventListener = NearbyServiceStreamListener( + final messagesListener = NearbyServiceMessagesListener( onCreated: () { updateState(AppState.communicationChannelCreated); }, - onMessage: (event) { + onData: (event) { listener?.call(event); }, - onFile: (event) async { - final content = event.content; - final fileAcceptFuture = _filesAccepts[content.id]; - - if (fileAcceptFuture != null && (await fileAcceptFuture == true)) { - final downloadsDir = Directory('storage/emulated/0/Download'); - final newFile = await event.file.copy( - '${downloadsDir.path}/${content.id}_${content.fileName}', - ); - onFileSaved?.call(newFile); - } - }, onError: (e, [StackTrace? s]) { stopListeningAll(); }, ); + 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); + }, + ); await _nearbyService.startCommunicationChannel( NearbyCommunicationChannelData( connectedDevice!.info.id, - eventListener: eventListener, + messagesListener: messagesListener, + filesListener: filesListener, ), ); } @@ -422,16 +422,29 @@ class AppService extends ChangeNotifier { ); } - void sendFile(String filePath) { + void sendFileRequest(String filePath) { if (connectedDevice == null) return; _nearbyService.send( OutgoingNearbyMessage( - content: NearbyMessageFileContent(filePath: filePath), + content: NearbyMessageFileRequest(filePath: filePath), receiver: connectedDevice!.info, ), ); } + void sendFileAccept(NearbyMessageFileRequest request) { + if (connectedDevice == null) return; + _nearbyService.send( + OutgoingNearbyMessage( + receiver: connectedDevice!.info, + content: NearbyMessageFileResponse.fromRequest( + request, + response: true, + ), + ), + ); + } + void setFileAcceptFuture(String id, Future future) { _filesAccepts[id] = future; } @@ -794,7 +807,7 @@ class _ConnectedBody extends StatelessWidget { void _listener(BuildContext context, ReceivedNearbyMessage message) { final senderSubtitle = 'From ${message.sender.displayName} ' '(ID: ${message.sender.id})'; - message.content.get( + message.content.byType( onText: (content) { AppShackBar.show( Scaffold.of(context).context, @@ -802,15 +815,16 @@ class _ConnectedBody extends StatelessWidget { subtitle: senderSubtitle, ); }, - onFile: (content) { - context.read().setFileAcceptFuture( - content.id, - ActionDialog.show( - context, - title: 'File request ${content.fileName}', - subtitle: senderSubtitle, - ), - ); + onFileRequest: (content) { + ActionDialog.show( + context, + title: 'File request ${content.fileName}', + subtitle: senderSubtitle, + ).then((value) { + if (value == true) { + context.read().sendFileAccept(content); + } + }); }, ); } @@ -907,7 +921,7 @@ class _ConnectedSocketBodyState extends State<_ConnectedSocketBody> { child: _ActionButton( title: 'Send', onTap: () { - service.sendFile(filePath); + service.sendFileRequest(filePath); }, ), ), diff --git a/lib/src/models/nearby_file.dart b/lib/src/models/nearby_file.dart index 2ebfe81..895e259 100644 --- a/lib/src/models/nearby_file.dart +++ b/lib/src/models/nearby_file.dart @@ -5,10 +5,10 @@ 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 [NearbyMessageFileContent.id]. +/// You can use [id] to compare it to the id from [NearbyMessageFileRequest.id]. /// /// From the communication channel, you usually get -/// the [NearbyMessageFileContent] request first. After that, you get [NearbyFile]. +/// the [NearbyMessageFileRequest] request first. After that, you get [NearbyFile]. /// class NearbyFile { /// diff --git a/lib/src/models/nearby_message_content.dart b/lib/src/models/nearby_message_content.dart index c7fb99a..202a6fe 100644 --- a/lib/src/models/nearby_message_content.dart +++ b/lib/src/models/nearby_message_content.dart @@ -5,12 +5,13 @@ import 'package:nearby_service/src/utils/random.dart'; /// Type of the message. /// /// If [text], it will be a text message. -/// If [file], it will be a file request. After accepting request, +/// If [fileRequest], it will be a file request. After accepting request, /// user can get file stream from connected device. /// enum NearbyMessageContentType { text, - file; + fileRequest, + fileResponse; /// /// Checks if this is [NearbyMessageContentType.text] @@ -20,10 +21,17 @@ enum NearbyMessageContentType { } /// - /// Checks if this is [NearbyMessageContentType.file] + /// Checks if this is [NearbyMessageContentType.fileRequest] /// - bool get isFile { - return this == NearbyMessageContentType.file; + bool get isFileRequest { + return this == NearbyMessageContentType.fileRequest; + } + + /// + /// Checks if this is [NearbyMessageContentType.fileResponse] + /// + bool get isFileResponse { + return this == NearbyMessageContentType.fileResponse; } } @@ -35,7 +43,7 @@ abstract class NearbyMessageContent { const NearbyMessageContent(this._type); /// - /// Contains the conditional logic of creating [NearbyMessageFileContent] + /// Contains the conditional logic of creating [NearbyMessageFileRequest] /// or [NearbyMessageTextContent] by `type` field of [json]. /// factory NearbyMessageContent.fromJson(Map? json) { @@ -43,10 +51,12 @@ abstract class NearbyMessageContent { final type = NearbyMessageContentType.values.firstWhere( (e) => e.name == json?['type'], ); - if (type.isFile) { - return NearbyMessageFileContent.fromJson(json); + if (type.isFileRequest) { + return NearbyMessageFileRequest.fromJson(json); } else if (type.isText) { return NearbyMessageTextContent.fromJson(json); + } else if (type.isFileResponse) { + return NearbyMessageFileResponse.fromJson(json); } else { throw NearbyServiceException.unsupportedDecoding(json); } @@ -66,17 +76,23 @@ abstract class NearbyMessageContent { /// * The [onText] callback returns this instance of [NearbyMessageContent], /// cast as [NearbyMessageTextContent] if is a text. /// - /// * The [onFile] callback returns this instance of [NearbyMessageContent], - /// cast as [NearbyMessageFileContent] if is a file. + /// * The [onFileRequest] callback returns this instance of [NearbyMessageContent], + /// cast as [NearbyMessageFileRequest] if is a file request. /// - T? get({ + /// * The [onFileResponse] callback returns this instance of [NearbyMessageContent], + /// cast as [NearbyMessageFileResponse] if is a file response. + /// + T? byType({ T Function(NearbyMessageTextContent)? onText, - T Function(NearbyMessageFileContent)? onFile, + T Function(NearbyMessageFileRequest)? onFileRequest, + T Function(NearbyMessageFileResponse)? onFileResponse, }) { if (this is NearbyMessageTextContent && onText != null) { return onText(this as NearbyMessageTextContent); - } else if (this is NearbyMessageFileContent && onFile != null) { - return onFile(this as NearbyMessageFileContent); + } else if (this is NearbyMessageFileRequest && onFileRequest != null) { + return onFileRequest(this as NearbyMessageFileRequest); + } else if (this is NearbyMessageFileResponse && onFileResponse != null) { + return onFileResponse(this as NearbyMessageFileResponse); } return null; } @@ -141,38 +157,12 @@ class NearbyMessageTextContent extends NearbyMessageContent { } } -/// -/// 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 NearbyMessageFileContent extends NearbyMessageContent { - const NearbyMessageFileContent._({ +abstract class NearbyMessageFileContent extends NearbyMessageContent { + const NearbyMessageFileContent( + super.type, { required this.id, required this.filePath, - }) : super( - NearbyMessageContentType.file, - ); - - /// - /// Basic constructor with [filePath] to be sent or received. - /// - NearbyMessageFileContent({required this.filePath}) - : id = RandomUtils.instance.nextInt(1000000, 9999999).toString(), - super( - NearbyMessageContentType.file, - ); - - /// - /// Gets [NearbyMessageFileContent] from [json]. - /// - factory NearbyMessageFileContent.fromJson(Map? json) { - return NearbyMessageFileContent._( - id: json?['id'] ?? '', - filePath: json?['filePath'] ?? '', - ); - } + }); /// /// ID for comparing file requests. @@ -192,23 +182,16 @@ class NearbyMessageFileContent extends NearbyMessageContent { } } - @override - bool get isValid => filePath.isNotEmpty; - @override bool operator ==(Object other) => identical(this, other) || other is NearbyMessageFileContent && runtimeType == other.runtimeType && + id == other.id && filePath == other.filePath; @override - int get hashCode => filePath.hashCode; - - @override - String toString() { - return 'NearbyMessageFileContent{filePath: $filePath, id: $id}'; - } + int get hashCode => id.hashCode ^ filePath.hashCode; @override Map toJson() { @@ -219,3 +202,92 @@ class NearbyMessageFileContent extends NearbyMessageContent { }; } } + +/// +/// 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 { + const NearbyMessageFileRequest._({ + required super.id, + required super.filePath, + }) : super( + NearbyMessageContentType.fileRequest, + ); + + /// + /// Basic constructor with [filePath] to be sent or received. + /// + NearbyMessageFileRequest({required super.filePath}) + : super( + NearbyMessageContentType.fileRequest, + id: RandomUtils.instance.nextInt(1000000, 9999999).toString(), + ); + + /// + /// Gets [NearbyMessageFileRequest] from [json]. + /// + factory NearbyMessageFileRequest.fromJson(Map? json) { + return NearbyMessageFileRequest._( + id: json?['id'] ?? '', + filePath: json?['filePath'] ?? '', + ); + } + + @override + bool get isValid => filePath.isNotEmpty; + + @override + String toString() { + return 'NearbyMessageFileRequest{id: $id, filePath: $filePath}'; + } +} + +class NearbyMessageFileResponse extends NearbyMessageFileContent { + NearbyMessageFileResponse({ + required super.id, + required super.filePath, + required this.response, + }) : super( + NearbyMessageContentType.fileResponse, + ); + + factory NearbyMessageFileResponse.fromRequest( + NearbyMessageFileRequest request, { + required bool response, + }) { + return NearbyMessageFileResponse( + id: request.id, + filePath: request.filePath, + response: response, + ); + } + + factory NearbyMessageFileResponse.fromJson(Map? json) { + return NearbyMessageFileResponse( + id: json?['id'] ?? '', + filePath: json?['filePath'] ?? '', + response: json?['response'] ?? false, + ); + } + + final bool response; + + @override + bool get isValid => filePath.isNotEmpty; + + @override + Map toJson() { + return { + 'response': response, + ...super.toJson(), + }; + } + + @override + String toString() { + return 'NearbyMessageFileResponse{response: $response, id: $id, filePath: $filePath}'; + } +} diff --git a/lib/src/platforms/android/nearby_android_service.dart b/lib/src/platforms/android/nearby_android_service.dart index 4a8a3a7..40a41ea 100644 --- a/lib/src/platforms/android/nearby_android_service.dart +++ b/lib/src/platforms/android/nearby_android_service.dart @@ -19,14 +19,6 @@ class NearbyAndroidService extends NearbyService { return _socketService.state; } - /// - /// The connection information of a Wi-Fi p2p group connection - /// from the socket service. - /// - NearbyConnectionAndroidInfo? get connectionInfo { - return _socketService.connectionInfo; - } - /// /// Initializes Android [WifiP2PManager](https://developer.android.com/reference/android/net/wifi/p2p/WifiP2pManager) /// diff --git a/lib/src/platforms/android/socket_service/file_creator.dart b/lib/src/platforms/android/socket_service/file_creator.dart deleted file mode 100644 index 57531ee..0000000 --- a/lib/src/platforms/android/socket_service/file_creator.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'dart:io'; - -import 'package:nearby_service/nearby_service.dart'; -import 'package:path_provider/path_provider.dart'; - -class FileCreator { - FileCreator({required this.content}); - - static const _finishCommand = '@@FINISH_SENDING_FILE_'; - - static String generateFinishCommand(String id) { - return '$_finishCommand$id'; - } - - final NearbyMessageFileContent content; - final _bytes = []; - - String get finishCommand => '$_finishCommand${content.id}'; - - void add(List value) { - _bytes.addAll(value); - } - - Future getFile() async { - final directory = await getTemporaryDirectory(); - final file = File('${directory.path}/${content.fileName}'); - - await file.writeAsBytes(_bytes); - return NearbyFile(file: file, content: content); - } -} diff --git a/lib/src/platforms/android/socket_service/file_sockets_manager.dart b/lib/src/platforms/android/socket_service/file_sockets_manager.dart new file mode 100644 index 0000000..7679b26 --- /dev/null +++ b/lib/src/platforms/android/socket_service/file_sockets_manager.dart @@ -0,0 +1,165 @@ +part of 'nearby_socket_service.dart'; + +class FileSocketsManager { + FileSocketsManager(this._network, this._service); + + final NearbyServiceNetwork _network; + final NearbyAndroidService _service; + final _filesSockets = []; + final _serverWaitingContents = {}; + + NearbyServiceFilesListener? _filesListener; + + void setListener(NearbyServiceFilesListener? listener) { + _filesListener = listener; + } + + void onWsRequest(HttpRequest request) { + final type = NearbySocketType.fromRequest(request); + final fileId = NearbyFileId.fromRequest(request); + if (type == NearbySocketType.file && fileId != null) { + _serverWaitingContents[fileId] = request; + } + } + + Future handleFileMessageContent( + NearbyMessageFileContent content, { + 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); + } + } + } + } + + Future closeAll() async { + for (final fileSocket in _filesSockets) { + await fileSocket.close(); + } + _filesSockets.clear(); + _filesListener = null; + } + + Future _handleClientFileContent( + NearbyMessageFileContent 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, + }, + ); + + 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 file ${content.fileName}', + ); + } else { + await Future.delayed( + connectionData.clientReconnectInterval, + () => _handleClientFileContent( + content, + connectionData: connectionData, + ownerIpAddress: ownerIpAddress, + ), + ); + } + } + } catch (e) { + Logger.error(e); + } + } + + Future _handleServerFileContent( + NearbyMessageFileContent content, + ) async { + MapEntry? request; + try { + request = _serverWaitingContents.entries.firstWhere( + (element) => element.key == content.id, + ); + } catch (e) { + request = null; + } + + 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, + ), + ); + _serverWaitingContents.remove(content.id); + Logger.info('Created file socket for ${content.fileName}'); + } + } + + 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); + + file.openRead().listen( + (data) { + fileSocket.sendData(data); + }, + onDone: () { + Logger.debug('Sent finished command for ${content.fileName}'); + fileSocket.sendData( + FilesSocket.generateFinishCommand(fileSocket.content.id), + ); + }, + ); + } + } + + FilesSocket? _find(String id) { + FilesSocket? fileSocket; + try { + fileSocket = _filesSockets.firstWhere( + (element) => element.content.id == id, + ); + } catch (e) { + fileSocket = null; + } + return fileSocket; + } +} 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 4e67e38..1baf244 100644 --- a/lib/src/platforms/android/socket_service/nearby_socket_service.dart +++ b/lib/src/platforms/android/socket_service/nearby_socket_service.dart @@ -4,7 +4,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:nearby_service/nearby_service.dart'; -import 'package:nearby_service/src/platforms/android/socket_service/file_creator.dart'; +import 'package:nearby_service/src/utils/file_socket.dart'; import 'package:nearby_service/src/utils/logger.dart'; import 'package:nearby_service/src/utils/random.dart'; import 'package:nearby_service/src/utils/stream_mapper.dart'; @@ -13,24 +13,28 @@ part 'ping_manager.dart'; part 'network.dart'; +part 'file_sockets_manager.dart'; + /// /// A service for creating a communication channel on the Android platform. /// class NearbySocketService { - NearbySocketService(this._manager); + NearbySocketService(this._service); - final NearbyAndroidService _manager; + final NearbyAndroidService _service; final _pingManager = NearbySocketPingManager(); final _network = NearbyServiceNetwork(); + late final _fileSocketsManager = FileSocketsManager(_network, _service); final state = ValueNotifier(CommunicationChannelState.notConnected); - NearbyConnectionAndroidInfo? connectionInfo; - FileCreator? fileCreator; + NearbyAndroidCommunicationChannelData _androidData = + const NearbyAndroidCommunicationChannelData(); + String? _connectedDeviceId; WebSocket? _socket; HttpServer? _server; - StreamSubscription? _streamSubscription; + StreamSubscription? _messagesSubscription; /// /// Start a socket with the user's role defined. @@ -46,24 +50,25 @@ class NearbySocketService { required NearbyCommunicationChannelData data, }) async { state.value = CommunicationChannelState.loading; + + _androidData = data.androidData; _connectedDeviceId = data.connectedDeviceId; - connectionInfo = await _manager.getConnectionInfo(); - if (connectionInfo != null && connectionInfo!.groupFormed) { - final androidData = data.androidData; - if (connectionInfo!.isGroupOwner) { + + _fileSocketsManager.setListener(data.filesListener); + + final info = await _service.getConnectionInfo(); + + if (info != null && info.groupFormed) { + if (info.isGroupOwner) { await _startServerSubscription( - serverListener: androidData.serverListener, - socketListener: data.eventListener, - info: connectionInfo!, - port: androidData.port, + socketListener: data.messagesListener, + info: info, ); return true; } else { await _tryConnectClient( - socketListener: data.eventListener, - reconnectInterval: androidData.clientReconnectInterval, - info: connectionInfo!, - port: androidData.port, + socketListener: data.messagesListener, + info: info, ); return true; } @@ -77,7 +82,7 @@ class NearbySocketService { Future send(OutgoingNearbyMessage message) async { if (message.isValid) { if (_socket != null && message.receiver.id == _connectedDeviceId) { - final sender = await _manager.getCurrentDeviceInfo(); + final sender = await _service.getCurrentDeviceInfo(); if (sender != null) { _socket!.add( jsonEncode( @@ -87,20 +92,13 @@ class NearbySocketService { }, ), ); - message.content.get( - onFile: (fileContent) { - final file = File(fileContent.filePath); - - file.openRead().listen( - (data) => _socket?.add(data), - onDone: () { - _socket?.add( - FileCreator.generateFinishCommand(fileContent.id), - ); - }, - ); - }, - ); + if (message.content is NearbyMessageFileContent) { + _fileSocketsManager.handleFileMessageContent( + message.content as NearbyMessageFileContent, + androidData: _androidData, + isReceived: false, + ); + } } return true; } @@ -111,17 +109,20 @@ class NearbySocketService { } /// - /// Turns off [_streamSubscription] and [_socket]. + /// Turns off [_messagesSubscription] and [_socket]. /// Future cancel() async { try { - await _streamSubscription?.cancel(); - _streamSubscription = null; + await _messagesSubscription?.cancel(); + await _fileSocketsManager.closeAll(); + + _messagesSubscription = null; _socket?.close(); _socket = null; _server?.close(force: true); _server = null; _connectedDeviceId = null; + state.value = CommunicationChannelState.notConnected; return true; } catch (e) { @@ -130,50 +131,45 @@ class NearbySocketService { } Future _tryConnectClient({ - required NearbyServiceStreamListener socketListener, + required NearbyServiceMessagesListener socketListener, required NearbyConnectionAndroidInfo info, - required int port, - required Duration reconnectInterval, }) async { final response = await _network.pingServer( address: info.ownerIpAddress, - port: port, + port: _androidData.port, ); if (await _pingManager.checkPong(response)) { _socket = await _network.connectToSocket( ownerIpAddress: info.ownerIpAddress, - port: port, + port: _androidData.port, + socketType: NearbySocketType.message, ); _createSocketSubscription(socketListener); } else { Logger.debug( - 'Retry to connect to the server in ${reconnectInterval.inSeconds}s', + 'Retry to connect to the server in ${_androidData.clientReconnectInterval.inSeconds}s', ); - Future.delayed(reconnectInterval, () { + Future.delayed(_androidData.clientReconnectInterval, () { _tryConnectClient( socketListener: socketListener, - reconnectInterval: reconnectInterval, info: info, - port: port, ); }); } } Future _startServerSubscription({ - required NearbyServiceStreamListener socketListener, + required NearbyServiceMessagesListener socketListener, required NearbyConnectionAndroidInfo info, - required int port, - ValueChanged? serverListener, }) async { _server = await _network.startServer( ownerIpAddress: info.ownerIpAddress, - port: port, + port: _androidData.port, ); _server?.listen( (request) async { - serverListener?.call(request); + _androidData.serverListener?.call(request); final isPing = await _pingManager.checkPing(request); if (isPing) { Logger.debug('Server got ping request'); @@ -182,8 +178,13 @@ class NearbySocketService { } if (request.uri.path == _Urls.ws) { - _socket = await WebSocketTransformer.upgrade(request); - _createSocketSubscription(socketListener); + final type = NearbySocketType.fromRequest(request); + if (type == NearbySocketType.message) { + _socket = await WebSocketTransformer.upgrade(request); + _createSocketSubscription(socketListener); + } else { + _fileSocketsManager.onWsRequest(request); + } } else { request.response ..statusCode = HttpStatus.notFound @@ -194,43 +195,44 @@ class NearbySocketService { ); } - void _createSocketSubscription(NearbyServiceStreamListener socketListener) { + void _createSocketSubscription(NearbyServiceMessagesListener socketListener) { Logger.debug('Starting socket subscription'); if (_connectedDeviceId != null) { - _streamSubscription = _socket?.listen( + _messagesSubscription = _socket?.listen( (event) async { - if (fileCreator != null) { - if (event is List) { - fileCreator!.add(event); - } else if (event == fileCreator?.finishCommand) { - final file = await fileCreator!.getFile().whenComplete( - () { - fileCreator = null; - }, + // if (fileLoaders != null) { + // if (event is List) { + // fileLoaders!.add(event); + // } else if (event == fileLoaders?.finishCommand) { + // final file = await fileLoaders!.getFile().whenComplete( + // () { + // fileLoaders = null; + // }, + // ); + // socketListener.onFile?.call(file); + // } + // } else { + try { + final message = MessagesStreamMapper.toMessage(event); + if (message != null) { + final newMessage = MessagesStreamMapper.replaceId( + message, + _connectedDeviceId!, ); - socketListener.onFile?.call(file); - } - } else { - try { - final message = MessagesStreamMapper.toMessage(event); - if (message != null) { - final newMessage = MessagesStreamMapper.replaceId( - message, - _connectedDeviceId!, + if (newMessage.content is NearbyMessageFileContent) { + _fileSocketsManager.handleFileMessageContent( + newMessage.content as NearbyMessageFileContent, + androidData: _androidData, + isReceived: true, ); - newMessage.content.get( - onFile: (fileContent) { - fileCreator = FileCreator(content: fileContent); - }, - ); - - socketListener.onMessage(newMessage); } - } catch (e) { - Logger.error(e); + socketListener.onData(newMessage); } + } catch (e) { + Logger.error(e); } + // } }, onDone: () { state.value = CommunicationChannelState.notConnected; @@ -244,7 +246,7 @@ class NearbySocketService { cancelOnError: socketListener.cancelOnError, ); } - if (_streamSubscription != null) { + if (_messagesSubscription != null) { state.value = CommunicationChannelState.connected; Logger.info('Socket subscription was created successfully'); socketListener.onCreated?.call(); diff --git a/lib/src/platforms/android/socket_service/network.dart b/lib/src/platforms/android/socket_service/network.dart index c5f6f5a..1d1a0e5 100644 --- a/lib/src/platforms/android/socket_service/network.dart +++ b/lib/src/platforms/android/socket_service/network.dart @@ -57,13 +57,21 @@ class NearbyServiceNetwork { Future connectToSocket({ required String ownerIpAddress, required int port, + required NearbySocketType socketType, + Map? headers, }) async { try { final connectionId = RandomUtils.instance.nextInt(100, 999); final url = '${_Protocols.ws}$ownerIpAddress:$port${_Urls.ws}?as=$connectionId'; Logger.debug('Connecting to $url'); - final socket = await WebSocket.connect(url); + final socket = await WebSocket.connect( + url, + headers: { + NearbySocketType.key: socketType.name, + ...?headers, + }, + ); Logger.info('Connected to $url'); return socket; } catch (e) { @@ -71,3 +79,32 @@ class NearbyServiceNetwork { } } } + +enum NearbySocketType { + message, + file; + + static const key = 'SocketType'; + + static NearbySocketType? fromRequest(HttpRequest request) { + return NearbySocketType.fromString( + request.headers.value(NearbySocketType.key), + ); + } + + static NearbySocketType? fromString(String? value) { + try { + return values.firstWhere((element) => element.name == value); + } catch (e) { + return null; + } + } +} + +class NearbyFileId { + static const key = 'FileID'; + + static String? fromRequest(HttpRequest request) { + return request.headers.value(NearbyFileId.key); + } +} diff --git a/lib/src/platforms/ios/nearby_ios_service.dart b/lib/src/platforms/ios/nearby_ios_service.dart index ae0943f..a102b79 100644 --- a/lib/src/platforms/ios/nearby_ios_service.dart +++ b/lib/src/platforms/ios/nearby_ios_service.dart @@ -163,7 +163,8 @@ class NearbyIOSService extends NearbyService { Logger.debug('Creating messages subscription'); _state.value = CommunicationChannelState.loading; await endCommunicationChannel(); - final eventListener = data.eventListener; + final eventListener = data.messagesListener; + final filesListener = data.filesListener; _messagesSubscription = NearbyServiceIOSPlatform.instance.messagesStream // .map(MessagesStreamMapper.toMessage) // .where((event) => event?.sender.id == data.connectedDeviceId) @@ -174,11 +175,11 @@ class NearbyIOSService extends NearbyService { try { final message = MessagesStreamMapper.toMessage(event); if (message != null && message.sender.id == data.connectedDeviceId) { - eventListener.onMessage(message); + eventListener.onData(message); } } catch (e) { try { - eventListener.onFile?.call(event); + filesListener?.onData(event); } catch (e) { Logger.error(e); } diff --git a/lib/src/types/communication_channel_data.dart b/lib/src/types/communication_channel_data.dart index 28a2806..6006b8a 100644 --- a/lib/src/types/communication_channel_data.dart +++ b/lib/src/types/communication_channel_data.dart @@ -16,7 +16,8 @@ class NearbyCommunicationChannelData { /// const NearbyCommunicationChannelData( this.connectedDeviceId, { - required this.eventListener, + required this.messagesListener, + this.filesListener, this.androidData = const NearbyAndroidCommunicationChannelData(), }); @@ -28,7 +29,12 @@ class NearbyCommunicationChannelData { /// /// Listener for message stream changes. /// - final NearbyServiceStreamListener eventListener; + final NearbyServiceMessagesListener messagesListener; + + /// + /// Listener for message stream changes. + /// + final NearbyServiceFilesListener? filesListener; /// /// Android-specific connection data. @@ -41,18 +47,18 @@ class NearbyCommunicationChannelData { other is NearbyCommunicationChannelData && runtimeType == other.runtimeType && connectedDeviceId == other.connectedDeviceId && - eventListener == other.eventListener && + messagesListener == other.messagesListener && androidData == other.androidData; @override int get hashCode => connectedDeviceId.hashCode ^ - eventListener.hashCode ^ + messagesListener.hashCode ^ androidData.hashCode; @override String toString() { - return 'NearbyCommunicationChannelData{connectedDeviceId: $connectedDeviceId, eventListener: $eventListener, androidData: $androidData}'; + return 'NearbyCommunicationChannelData{connectedDeviceId: $connectedDeviceId, eventListener: $messagesListener, androidData: $androidData}'; } } diff --git a/lib/src/types/nearby_service_stream_listener.dart b/lib/src/types/nearby_service_stream_listener.dart index 05a1c88..0592c57 100644 --- a/lib/src/types/nearby_service_stream_listener.dart +++ b/lib/src/types/nearby_service_stream_listener.dart @@ -4,24 +4,58 @@ import 'package:nearby_service/nearby_service.dart'; /// /// Stream Subscription Listener. /// -class NearbyServiceStreamListener { +class NearbyServiceSocketListener { /// - /// It is required to pass the [onMessage] parameter to process the + /// It is required to pass the [onData] parameter to process the /// data that came through the stream. /// - const NearbyServiceStreamListener({ - required this.onMessage, - this.onFile, + const NearbyServiceSocketListener({ + required this.onData, this.onCreated, this.onDone, this.onError, this.cancelOnError, }); - final ValueChanged onMessage; - final ValueChanged? onFile; + final ValueChanged onData; final VoidCallback? onCreated; final VoidCallback? onDone; final void Function(Object, [StackTrace])? onError; final bool? cancelOnError; } + +/// +/// Stream Subscription Listener. +/// +class NearbyServiceMessagesListener + extends NearbyServiceSocketListener { + /// + /// It is required to pass the [onData] parameter to process the + /// data that came through the stream. + /// + const NearbyServiceMessagesListener({ + required super.onData, + super.onCreated, + super.onDone, + super.onError, + super.cancelOnError, + }); +} + +/// +/// Stream Subscription Listener. +/// +class NearbyServiceFilesListener + extends NearbyServiceSocketListener { + /// + /// It is required to pass the [onData] parameter to process the + /// data that came through the stream. + /// + const NearbyServiceFilesListener({ + required super.onData, + super.onCreated, + super.onDone, + super.onError, + super.cancelOnError, + }); +} diff --git a/lib/src/utils/file_socket.dart b/lib/src/utils/file_socket.dart new file mode 100644 index 0000000..cc93468 --- /dev/null +++ b/lib/src/utils/file_socket.dart @@ -0,0 +1,77 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:nearby_service/nearby_service.dart'; +import 'package:nearby_service/src/utils/logger.dart'; +import 'package:path_provider/path_provider.dart'; + +class FilesSocket { + FilesSocket.startListening({ + required this.content, + required WebSocket socket, + required NearbyServiceFilesListener? listener, + required void Function(FilesSocket) onDestroy, + }) : _socket = socket { + _socket.listen( + (event) async { + if (event is List) { + saveChunk(event); + } else if (event == finishCommand) { + final file = await getFile().whenComplete( + () { + onDestroy(this); + }, + ); + Logger.info('File ${file.content.fileName} was created'); + listener?.onData.call(file); + } + }, + onError: listener?.onError, + cancelOnError: listener?.cancelOnError, + onDone: listener?.onDone, + ); + listener?.onCreated?.call(); + } + + static const _finishCommand = '@@FINISH_SENDING_FILE_'; + + static String generateFinishCommand(String id) { + return '$_finishCommand$id'; + } + + final WebSocket _socket; + final NearbyMessageFileContent content; + final _bytes = []; + + int chunksCount = 0; + + String get finishCommand => '$_finishCommand${content.id}'; + + 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}'); + } + } + + Future getFile() async { + try { + final directory = await getTemporaryDirectory(); + final file = File('${directory.path}/${content.fileName}'); + + await file.writeAsBytes(_bytes); + return NearbyFile(file: file, content: content); + } catch (e) { + Logger.error(e); + rethrow; + } + } + + Future close() => _socket.close(); +}