diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index d2391e7..c29e0a9 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -27,7 +27,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { namespace "com.xenikii.nearby_service_example" - compileSdk flutter.compileSdkVersion + compileSdk 34 ndkVersion flutter.ndkVersion compileOptions { diff --git a/example/android/build.gradle b/example/android/build.gradle index ce647a4..ab1fdb4 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '1.9.22' repositories { google() mavenCentral() diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 4760cc1..d5d971a 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -26,6 +26,10 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + LSSupportsOpeningDocumentsInPlace + + UIFileSharingEnabled + UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName diff --git a/example/lib/components/action_dialog.dart b/example/lib/components/action_dialog.dart new file mode 100644 index 0000000..1483b12 --- /dev/null +++ b/example/lib/components/action_dialog.dart @@ -0,0 +1,31 @@ +part of '../main.dart'; + +class ActionDialog { + ActionDialog._(); + + static Future show( + BuildContext context, { + required String title, + required String subtitle, + }) { + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(title), + content: Text(subtitle), + actions: [ + ElevatedButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Yes'), + ), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('No'), + ), + ], + ); + }, + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 70d598e..8f97c3b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'dart:async'; @@ -11,6 +12,8 @@ import 'components/app_snack_bar.dart'; part 'components/action_button.dart'; +part 'components/action_dialog.dart'; + Future main() async { WidgetsFlutterBinding.ensureInitialized(); final service = AppService(); @@ -197,6 +200,8 @@ class AppService extends ChangeNotifier { StreamSubscription? peersSubscription; StreamSubscription? connectedDeviceSubscription; + final _filesAccepts = >{}; + @override void dispose() { stopListeningAll(); @@ -373,14 +378,27 @@ class AppService extends ChangeNotifier { Future startCommunicationChannel({ ValueChanged? listener, + ValueChanged? onFileSaved, }) async { final eventListener = NearbyServiceStreamListener( - onCreated: (_) { + onCreated: () { updateState(AppState.communicationChannelCreated); }, - onData: (event) { + onMessage: (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(); }, @@ -394,16 +412,30 @@ class AppService extends ChangeNotifier { ); } - void send(String message) { + void sendMessage(String message) { if (connectedDevice == null) return; _nearbyService.send( OutgoingNearbyMessage( - value: message, + content: NearbyMessageTextContent(value: message), receiver: connectedDevice!.info, ), ); } + void sendFile(String filePath) { + if (connectedDevice == null) return; + _nearbyService.send( + OutgoingNearbyMessage( + content: NearbyMessageFileContent(filePath: filePath), + receiver: connectedDevice!.info, + ), + ); + } + + void setFileAcceptFuture(String id, Future future) { + _filesAccepts[id] = future; + } + Future disconnect(NearbyDevice device) async { try { await _nearbyService.disconnect(device); @@ -740,15 +772,11 @@ class _ConnectedBody extends StatelessWidget { if (service.communicationChannelState != CommunicationChannelState.loading) _ActionButton( - onTap: () => service.startCommunicationChannel( - listener: (event) => AppShackBar.show( - Scaffold.of(context).context, - event.value, - subtitle: 'From ${event.sender.displayName} ' - '(ID: ${event.sender.id})', - ), - ), title: 'Start communicate', + onTap: () => service.startCommunicationChannel( + listener: (event) => _listener(context, event), + onFileSaved: (file) => _onFileSaved(context, file), + ), ) else Text( @@ -762,6 +790,37 @@ class _ConnectedBody extends StatelessWidget { }, ); } + + void _listener(BuildContext context, ReceivedNearbyMessage message) { + final senderSubtitle = 'From ${message.sender.displayName} ' + '(ID: ${message.sender.id})'; + message.content.get( + onText: (content) { + AppShackBar.show( + Scaffold.of(context).context, + content.value, + subtitle: senderSubtitle, + ); + }, + onFile: (content) { + context.read().setFileAcceptFuture( + content.id, + ActionDialog.show( + context, + title: 'File request ${content.fileName}', + subtitle: senderSubtitle, + ), + ); + }, + ); + } + + void _onFileSaved(BuildContext context, File file) { + AppShackBar.show( + Scaffold.of(context).context, + 'File saved to ${file.path}', + ); + } } class _ConnectedSocketBody extends StatefulWidget { @@ -773,6 +832,7 @@ class _ConnectedSocketBody extends StatefulWidget { class _ConnectedSocketBodyState extends State<_ConnectedSocketBody> { String message = ''; + String filePath = ''; @override Widget build(BuildContext context) { @@ -797,6 +857,7 @@ class _ConnectedSocketBodyState extends State<_ConnectedSocketBody> { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( + flex: 2, child: TextField( onChanged: (value) => setState(() { message = value; @@ -810,15 +871,51 @@ class _ConnectedSocketBodyState extends State<_ConnectedSocketBody> { ), ), const SizedBox(width: 10), - _ActionButton( - title: 'Send', - onTap: () { - service.send(message); - }, + Flexible( + child: _ActionButton( + title: 'Send', + onTap: () { + service.sendMessage(message); + }, + ), ), ], ), ), + const SizedBox(height: 10), + Flexible( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 2, + child: _ActionButton( + type: _ActionButtonType.warning, + title: 'Choose a file', + onTap: () async { + final result = await FilePicker.platform.pickFiles(); + if (result != null && result.isSinglePick) { + setState(() { + filePath = result.paths.first!; + }); + } + }, + ), + ), + const SizedBox(width: 10), + Flexible( + child: _ActionButton( + title: 'Send', + onTap: () { + service.sendFile(filePath); + }, + ), + ), + ], + ), + ), + const SizedBox(height: 10), + Text('Selected file: $filePath'), ], ); }, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c05d17f..f5cec58 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -5,16 +5,17 @@ description: Demonstrates how to use the nearby_service plugin. publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: - sdk: '>=3.0.6 <4.0.0' + sdk: '>=3.0.0 <4.0.0' dependencies: + provider: ^6.1.1 + file_picker: ^6.1.1 flutter: sdk: flutter nearby_service: path: ../ - permission_handler: ^11.0.1 - provider: ^6.1.1 + dev_dependencies: diff --git a/ios/Classes/NearbyMessageConverter.swift b/ios/Classes/NearbyMessageConverter.swift index 4f05f39..5aae884 100644 --- a/ios/Classes/NearbyMessageConverter.swift +++ b/ios/Classes/NearbyMessageConverter.swift @@ -41,7 +41,7 @@ class NearbyMessageConverter { var result: String? let jsonObject = [ - "message": message, + "content": ["value": message, "type": "text"], "sender": ["id": peerID.displayName, "displayName": name], ] as [String : Any] diff --git a/ios/Classes/NearbyServicePlugin.swift b/ios/Classes/NearbyServicePlugin.swift index 8a03528..296156c 100644 --- a/ios/Classes/NearbyServicePlugin.swift +++ b/ios/Classes/NearbyServicePlugin.swift @@ -102,7 +102,8 @@ public class NearbyServicePlugin: NSObject, FlutterPlugin { } case "send": - if let message: String = getArgument(for: "message", call: call) { + if let content: Dictionary = getArgument(for: "content", call: call), + let message: String = content["value"] as? String { if let receiver : Dictionary = getArgument(for: "receiver", call: call), let receiverId : String = receiver["id"] as? String { diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index f8a24de..5324aa8 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -1,4 +1,6 @@ export 'nearby_device.dart'; export 'nearby_device_status.dart'; export 'nearby_message.dart'; +export 'nearby_message_content.dart'; export 'communication_channel_state.dart'; +export 'nearby_file.dart'; diff --git a/lib/src/models/nearby_device.dart b/lib/src/models/nearby_device.dart index 9566276..afeabd5 100644 --- a/lib/src/models/nearby_device.dart +++ b/lib/src/models/nearby_device.dart @@ -147,4 +147,20 @@ class NearbyDeviceInfo { 'displayName': displayName, }; } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is NearbyDeviceInfo && + runtimeType == other.runtimeType && + displayName == other.displayName && + id == other.id; + + @override + int get hashCode => displayName.hashCode ^ id.hashCode; + + @override + String toString() { + return 'NearbyDeviceInfo{displayName: $displayName, id: $id}'; + } } diff --git a/lib/src/models/nearby_file.dart b/lib/src/models/nearby_file.dart new file mode 100644 index 0000000..2ebfe81 --- /dev/null +++ b/lib/src/models/nearby_file.dart @@ -0,0 +1,47 @@ +import 'dart:io'; +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]. +/// +/// From the communication channel, you usually get +/// the [NearbyMessageFileContent] request first. After that, you get [NearbyFile]. +/// +class NearbyFile { + /// + /// Pass [content] assigned to file to be sent. + /// + const NearbyFile({ + required this.content, + required this.file, + }); + + /// + /// Quick info about the file + /// + final NearbyMessageFileContent content; + + /// + /// A file that you can save in your phone if needed + /// + final File file; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is NearbyFile && + runtimeType == other.runtimeType && + content == other.content && + file == other.file; + + @override + int get hashCode => content.hashCode ^ file.hashCode; + + @override + String toString() { + return 'NearbyFile{content: $content, file: $file}'; + } +} diff --git a/lib/src/models/nearby_message.dart b/lib/src/models/nearby_message.dart index d1a9756..3df8f89 100644 --- a/lib/src/models/nearby_message.dart +++ b/lib/src/models/nearby_message.dart @@ -5,18 +5,21 @@ import 'package:nearby_service/nearby_service.dart'; /// abstract class NearbyMessage { /// - /// The basic message contains only [value] - the content + /// The basic message contains only [content] - the content /// to be sent or received. /// - const NearbyMessage({required this.value}); - - final String value; + const NearbyMessage({required this.content}); /// - /// Checks if [value] is not empty + /// Model representing content to be sent or received + /// + final NearbyMessageContent content; + + /// + /// Checks if [content] is not empty /// bool get isValid { - return value.isNotEmpty; + return content.isValid; } @override @@ -24,14 +27,14 @@ abstract class NearbyMessage { identical(this, other) || other is NearbyMessage && runtimeType == other.runtimeType && - value == other.value; + content == other.content; @override - int get hashCode => value.hashCode; + int get hashCode => content.hashCode; @override String toString() { - return 'NearbyMessage{value: $value}'; + return 'NearbyMessage{content: $content}'; } } @@ -40,11 +43,11 @@ abstract class NearbyMessage { /// class OutgoingNearbyMessage extends NearbyMessage { /// - /// To send a message, in addition to [value], you need to pass [receiver] + /// To send a message, in addition to [content], you need to pass [receiver] /// to know to whom the message is addressed. /// const OutgoingNearbyMessage({ - required super.value, + required super.content, required this.receiver, }); @@ -58,7 +61,7 @@ class OutgoingNearbyMessage extends NearbyMessage { /// Map toJson() { return { - 'message': value, + 'content': content.toJson(), 'receiver': receiver.toJson(), }; } @@ -73,6 +76,11 @@ class OutgoingNearbyMessage extends NearbyMessage { @override int get hashCode => super.hashCode ^ receiver.hashCode; + + @override + String toString() { + return 'OutgoingNearbyMessage{receiver: $receiver content:$content}'; + } } /// @@ -80,11 +88,11 @@ class OutgoingNearbyMessage extends NearbyMessage { /// class ReceivedNearbyMessage extends NearbyMessage { /// - /// The received message contains a [sender] in addition to [value], + /// The received message contains a [sender] in addition to [content], /// to know from whom the message came. /// const ReceivedNearbyMessage({ - required super.value, + required super.content, required this.sender, }); @@ -93,7 +101,7 @@ class ReceivedNearbyMessage extends NearbyMessage { /// factory ReceivedNearbyMessage.fromJson(Map? json) { return ReceivedNearbyMessage( - value: json?['message'] ?? '', + content: NearbyMessageContent.fromJson(json?['content']), sender: NearbyDeviceInfo.fromJson(json?['sender']), ); } @@ -116,6 +124,6 @@ class ReceivedNearbyMessage extends NearbyMessage { @override String toString() { - return 'ReceivedNearbyMessage{sender: $sender}'; + return 'ReceivedNearbyMessage{sender: $sender content: $content}'; } } diff --git a/lib/src/models/nearby_message_content.dart b/lib/src/models/nearby_message_content.dart new file mode 100644 index 0000000..c7fb99a --- /dev/null +++ b/lib/src/models/nearby_message_content.dart @@ -0,0 +1,221 @@ +import 'package:nearby_service/nearby_service.dart'; +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, +/// user can get file stream from connected device. +/// +enum NearbyMessageContentType { + text, + file; + + /// + /// Checks if this is [NearbyMessageContentType.text] + /// + bool get isText { + return this == NearbyMessageContentType.text; + } + + /// + /// Checks if this is [NearbyMessageContentType.file] + /// + bool get isFile { + return this == NearbyMessageContentType.file; + } +} + +/// +/// Abstraction for the message content. +/// Contains [_type] to determine, what type of content is it. +/// +abstract class NearbyMessageContent { + const NearbyMessageContent(this._type); + + /// + /// Contains the conditional logic of creating [NearbyMessageFileContent] + /// or [NearbyMessageTextContent] by `type` field of [json]. + /// + factory NearbyMessageContent.fromJson(Map? json) { + try { + final type = NearbyMessageContentType.values.firstWhere( + (e) => e.name == json?['type'], + ); + if (type.isFile) { + return NearbyMessageFileContent.fromJson(json); + } else if (type.isText) { + return NearbyMessageTextContent.fromJson(json); + } else { + throw NearbyServiceException.unsupportedDecoding(json); + } + } catch (e) { + throw NearbyServiceException(e); + } + } + + final NearbyMessageContentType _type; + + /// + /// Check for the content if it is valid for sending or receiving. + /// + bool get isValid; + + /// + /// * 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. + /// + T? get({ + T Function(NearbyMessageTextContent)? onText, + T Function(NearbyMessageFileContent)? onFile, + }) { + if (this is NearbyMessageTextContent && onText != null) { + return onText(this as NearbyMessageTextContent); + } else if (this is NearbyMessageFileContent && onFile != null) { + return onFile(this as NearbyMessageFileContent); + } + return null; + } + + /// + /// Gets [Map] from [NearbyMessageContent] + /// + Map toJson() { + return {'type': _type.name}; + } +} + +/// +/// Nearby message Text content. +/// +/// Contains [value] - the message to be sent or received. +/// +class NearbyMessageTextContent extends NearbyMessageContent { + const NearbyMessageTextContent({required this.value}) + : super( + NearbyMessageContentType.text, + ); + + /// + /// Gets [NearbyMessageTextContent] from [json] + /// + factory NearbyMessageTextContent.fromJson(Map? json) { + return NearbyMessageTextContent( + value: json?['value'] ?? '', + ); + } + + /// + /// The message to be sent or received + /// + final String value; + + @override + bool get isValid => value.isNotEmpty; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is NearbyMessageTextContent && + runtimeType == other.runtimeType && + value == other.value; + + @override + int get hashCode => value.hashCode; + + @override + String toString() { + return 'NearbyMessageTextContent{value: $value}'; + } + + @override + Map toJson() { + return { + 'value': value, + ...super.toJson(), + }; + } +} + +/// +/// 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._({ + 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. + /// + 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 get isValid => filePath.isNotEmpty; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is NearbyMessageFileContent && + runtimeType == other.runtimeType && + filePath == other.filePath; + + @override + int get hashCode => filePath.hashCode; + + @override + String toString() { + return 'NearbyMessageFileContent{filePath: $filePath, id: $id}'; + } + + @override + Map toJson() { + return { + 'id': id, + 'filePath': filePath, + ...super.toJson(), + }; + } +} diff --git a/lib/src/platforms/android/socket_service/file_creator.dart b/lib/src/platforms/android/socket_service/file_creator.dart new file mode 100644 index 0000000..57531ee --- /dev/null +++ b/lib/src/platforms/android/socket_service/file_creator.dart @@ -0,0 +1,31 @@ +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/nearby_socket_service.dart b/lib/src/platforms/android/socket_service/nearby_socket_service.dart index 602a98f..4e67e38 100644 --- a/lib/src/platforms/android/socket_service/nearby_socket_service.dart +++ b/lib/src/platforms/android/socket_service/nearby_socket_service.dart @@ -1,11 +1,12 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'dart:math'; 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/logger.dart'; +import 'package:nearby_service/src/utils/random.dart'; import 'package:nearby_service/src/utils/stream_mapper.dart'; part 'ping_manager.dart'; @@ -25,10 +26,11 @@ class NearbySocketService { final state = ValueNotifier(CommunicationChannelState.notConnected); NearbyConnectionAndroidInfo? connectionInfo; + FileCreator? fileCreator; String? _connectedDeviceId; WebSocket? _socket; HttpServer? _server; - StreamSubscription? _messagesSubscription; + StreamSubscription? _streamSubscription; /// /// Start a socket with the user's role defined. @@ -80,27 +82,41 @@ class NearbySocketService { _socket!.add( jsonEncode( { - 'message': message.value, + 'content': message.content.toJson(), 'sender': sender.toJson(), }, ), ); + message.content.get( + onFile: (fileContent) { + final file = File(fileContent.filePath); + + file.openRead().listen( + (data) => _socket?.add(data), + onDone: () { + _socket?.add( + FileCreator.generateFinishCommand(fileContent.id), + ); + }, + ); + }, + ); } return true; } return false; } else { - throw NearbyServiceException.invalidMessage(message.value); + throw NearbyServiceException.invalidMessage(message.content); } } /// - /// Turns off [_messagesSubscription] and [_socket]. + /// Turns off [_streamSubscription] and [_socket]. /// Future cancel() async { try { - await _messagesSubscription?.cancel(); - _messagesSubscription = null; + await _streamSubscription?.cancel(); + _streamSubscription = null; _socket?.close(); _socket = null; _server?.close(force: true); @@ -182,13 +198,40 @@ class NearbySocketService { Logger.debug('Starting socket subscription'); if (_connectedDeviceId != null) { - _messagesSubscription = _socket - ?.map(MessagesStreamMapper.toMessage) - .where((event) => event != null) - .cast() - .map((e) => MessagesStreamMapper.replaceId(e, _connectedDeviceId!)) - .listen( - socketListener.onData, + _streamSubscription = _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; + }, + ); + socketListener.onFile?.call(file); + } + } else { + try { + final message = MessagesStreamMapper.toMessage(event); + if (message != null) { + final newMessage = MessagesStreamMapper.replaceId( + message, + _connectedDeviceId!, + ); + newMessage.content.get( + onFile: (fileContent) { + fileCreator = FileCreator(content: fileContent); + }, + ); + + socketListener.onMessage(newMessage); + } + } catch (e) { + Logger.error(e); + } + } + }, onDone: () { state.value = CommunicationChannelState.notConnected; socketListener.onDone?.call(); @@ -201,10 +244,10 @@ class NearbySocketService { cancelOnError: socketListener.cancelOnError, ); } - if (_messagesSubscription != null) { + if (_streamSubscription != null) { state.value = CommunicationChannelState.connected; Logger.info('Socket subscription was created successfully'); - socketListener.onCreated?.call(_messagesSubscription!); + socketListener.onCreated?.call(); } else { state.value = CommunicationChannelState.notConnected; } diff --git a/lib/src/platforms/android/socket_service/network.dart b/lib/src/platforms/android/socket_service/network.dart index ab6d618..c5f6f5a 100644 --- a/lib/src/platforms/android/socket_service/network.dart +++ b/lib/src/platforms/android/socket_service/network.dart @@ -11,7 +11,6 @@ class _Urls { class NearbyServiceNetwork { final _httpClient = HttpClient(); - final _random = Random(); Future pingServer({ required String address, @@ -60,7 +59,7 @@ class NearbyServiceNetwork { required int port, }) async { try { - final connectionId = _random.nextInt(1000) + 100; + final connectionId = RandomUtils.instance.nextInt(100, 999); final url = '${_Protocols.ws}$ownerIpAddress:$port${_Urls.ws}?as=$connectionId'; Logger.debug('Connecting to $url'); diff --git a/lib/src/platforms/ios/nearby_ios_service.dart b/lib/src/platforms/ios/nearby_ios_service.dart index 012aa02..ae0943f 100644 --- a/lib/src/platforms/ios/nearby_ios_service.dart +++ b/lib/src/platforms/ios/nearby_ios_service.dart @@ -16,7 +16,7 @@ class NearbyIOSService extends NearbyService { final _isBrowser = ValueNotifier(true); final _state = ValueNotifier(CommunicationChannelState.notConnected); - StreamSubscription? _messagesSubscription; + StreamSubscription? _messagesSubscription; @override ValueListenable get communicationChannelState => @@ -165,12 +165,25 @@ class NearbyIOSService extends NearbyService { await endCommunicationChannel(); final eventListener = data.eventListener; _messagesSubscription = NearbyServiceIOSPlatform.instance.messagesStream - .map(MessagesStreamMapper.toMessage) - .where((event) => event?.sender.id == data.connectedDeviceId) - .where((event) => event != null) - .cast() + // .map(MessagesStreamMapper.toMessage) + // .where((event) => event?.sender.id == data.connectedDeviceId) + // .where((event) => event != null) + // .cast() .listen( - eventListener.onData, + (event) { + try { + final message = MessagesStreamMapper.toMessage(event); + if (message != null && message.sender.id == data.connectedDeviceId) { + eventListener.onMessage(message); + } + } catch (e) { + try { + eventListener.onFile?.call(event); + } catch (e) { + Logger.error(e); + } + } + }, onDone: () { _state.value = CommunicationChannelState.notConnected; eventListener.onDone?.call(); @@ -184,7 +197,7 @@ class NearbyIOSService extends NearbyService { ); if (_messagesSubscription != null) { Logger.info('Messages subscription was created successfully'); - eventListener.onCreated?.call(_messagesSubscription!); + eventListener.onCreated?.call(); _state.value = CommunicationChannelState.connected; } else { _state.value = CommunicationChannelState.notConnected; @@ -215,7 +228,7 @@ class NearbyIOSService extends NearbyService { if (message.isValid) { return NearbyServiceIOSPlatform.instance.send(message); } - throw NearbyServiceException.invalidMessage(message.value); + throw NearbyServiceException.invalidMessage(message.content); } /// diff --git a/lib/src/types/nearby_service_stream_listener.dart b/lib/src/types/nearby_service_stream_listener.dart index 8a2363f..05a1c88 100644 --- a/lib/src/types/nearby_service_stream_listener.dart +++ b/lib/src/types/nearby_service_stream_listener.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:nearby_service/nearby_service.dart'; @@ -8,19 +6,21 @@ import 'package:nearby_service/nearby_service.dart'; /// class NearbyServiceStreamListener { /// - /// It is required to pass the [onData] parameter to process the + /// It is required to pass the [onMessage] parameter to process the /// data that came through the stream. /// const NearbyServiceStreamListener({ - required this.onData, + required this.onMessage, + this.onFile, this.onCreated, this.onDone, this.onError, this.cancelOnError, }); - final ValueChanged onData; - final ValueChanged>? onCreated; + final ValueChanged onMessage; + final ValueChanged? onFile; + final VoidCallback? onCreated; final VoidCallback? onDone; final void Function(Object, [StackTrace])? onError; final bool? cancelOnError; diff --git a/lib/src/utils/exception.dart b/lib/src/utils/exception.dart index ddafce7..7145801 100644 --- a/lib/src/utils/exception.dart +++ b/lib/src/utils/exception.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:nearby_service/nearby_service.dart'; import 'package:nearby_service/src/utils/logger.dart'; /// @@ -30,9 +31,9 @@ class NearbyServiceException implements Exception { ); } - factory NearbyServiceException.invalidMessage(String value) { + factory NearbyServiceException.invalidMessage(NearbyMessageContent content) { return NearbyServiceException( - 'The message="$value" is not valid', + 'The message="$content" is not valid', ); } diff --git a/lib/src/utils/random.dart b/lib/src/utils/random.dart new file mode 100644 index 0000000..f59fd33 --- /dev/null +++ b/lib/src/utils/random.dart @@ -0,0 +1,15 @@ +import 'dart:math'; + +class RandomUtils { + static RandomUtils? _instance; + + static RandomUtils get instance { + return _instance ?? RandomUtils(); + } + + final _random = Random(); + + int nextInt(int min, int max) { + return _random.nextInt(max - min) + min; + } +} diff --git a/lib/src/utils/stream_mapper.dart b/lib/src/utils/stream_mapper.dart index 51096a7..7da4b11 100644 --- a/lib/src/utils/stream_mapper.dart +++ b/lib/src/utils/stream_mapper.dart @@ -9,7 +9,7 @@ abstract class MessagesStreamMapper { String id, ) { return ReceivedNearbyMessage( - value: message.value, + content: message.content, sender: NearbyDeviceInfo( id: id, displayName: message.sender.displayName, diff --git a/pubspec.yaml b/pubspec.yaml index 8d4943c..5b7f93b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ environment: dependencies: flutter: sdk: flutter + path_provider: ^2.1.2 plugin_platform_interface: ^2.0.2 dev_dependencies: