feat(android): implement file sending logic
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
ext.kotlin_version = '1.9.22'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ActionDialog {
|
||||
ActionDialog._();
|
||||
|
||||
static Future<bool?> show(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String subtitle,
|
||||
}) {
|
||||
return showDialog<bool>(
|
||||
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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+114
-17
@@ -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<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
final service = AppService();
|
||||
@@ -197,6 +200,8 @@ class AppService extends ChangeNotifier {
|
||||
StreamSubscription? peersSubscription;
|
||||
StreamSubscription? connectedDeviceSubscription;
|
||||
|
||||
final _filesAccepts = <String, Future<bool?>>{};
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
stopListeningAll();
|
||||
@@ -373,14 +378,27 @@ class AppService extends ChangeNotifier {
|
||||
|
||||
Future<void> startCommunicationChannel({
|
||||
ValueChanged<ReceivedNearbyMessage>? listener,
|
||||
ValueChanged<File>? 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<bool?> future) {
|
||||
_filesAccepts[id] = future;
|
||||
}
|
||||
|
||||
Future<void> 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<AppService>().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'),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -102,7 +102,8 @@ public class NearbyServicePlugin: NSObject, FlutterPlugin {
|
||||
}
|
||||
|
||||
case "send":
|
||||
if let message: String = getArgument(for: "message", call: call) {
|
||||
if let content: Dictionary<String, AnyObject> = getArgument(for: "content", call: call),
|
||||
let message: String = content["value"] as? String {
|
||||
if let receiver : Dictionary<String, AnyObject> = getArgument(for: "receiver", call: call),
|
||||
let receiverId : String = receiver["id"] as? String
|
||||
{
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}';
|
||||
}
|
||||
}
|
||||
@@ -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<String, dynamic> 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<String, dynamic>? 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}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, dynamic>? 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>({
|
||||
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<String, dynamic> 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<String, dynamic>? 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<String, dynamic> 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<String, dynamic>? 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<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'filePath': filePath,
|
||||
...super.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 = <int>[];
|
||||
|
||||
String get finishCommand => '$_finishCommand${content.id}';
|
||||
|
||||
void add(List<int> value) {
|
||||
_bytes.addAll(value);
|
||||
}
|
||||
|
||||
Future<NearbyFile> getFile() async {
|
||||
final directory = await getTemporaryDirectory();
|
||||
final file = File('${directory.path}/${content.fileName}');
|
||||
|
||||
await file.writeAsBytes(_bytes);
|
||||
return NearbyFile(file: file, content: content);
|
||||
}
|
||||
}
|
||||
@@ -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<ReceivedNearbyMessage>? _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<bool> 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<ReceivedNearbyMessage>()
|
||||
.map((e) => MessagesStreamMapper.replaceId(e, _connectedDeviceId!))
|
||||
.listen(
|
||||
socketListener.onData,
|
||||
_streamSubscription = _socket?.listen(
|
||||
(event) async {
|
||||
if (fileCreator != null) {
|
||||
if (event is List<int>) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ class _Urls {
|
||||
|
||||
class NearbyServiceNetwork {
|
||||
final _httpClient = HttpClient();
|
||||
final _random = Random();
|
||||
|
||||
Future<HttpClientResponse?> 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');
|
||||
|
||||
@@ -16,7 +16,7 @@ class NearbyIOSService extends NearbyService {
|
||||
final _isBrowser = ValueNotifier<bool>(true);
|
||||
final _state = ValueNotifier(CommunicationChannelState.notConnected);
|
||||
|
||||
StreamSubscription<ReceivedNearbyMessage>? _messagesSubscription;
|
||||
StreamSubscription? _messagesSubscription;
|
||||
|
||||
@override
|
||||
ValueListenable<CommunicationChannelState> 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<ReceivedNearbyMessage>()
|
||||
// .map(MessagesStreamMapper.toMessage)
|
||||
// .where((event) => event?.sender.id == data.connectedDeviceId)
|
||||
// .where((event) => event != null)
|
||||
// .cast<ReceivedNearbyMessage>()
|
||||
.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);
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
@@ -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<ReceivedNearbyMessage> onData;
|
||||
final ValueChanged<StreamSubscription<ReceivedNearbyMessage>>? onCreated;
|
||||
final ValueChanged<ReceivedNearbyMessage> onMessage;
|
||||
final ValueChanged<NearbyFile>? onFile;
|
||||
final VoidCallback? onCreated;
|
||||
final VoidCallback? onDone;
|
||||
final void Function(Object, [StackTrace])? onError;
|
||||
final bool? cancelOnError;
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -14,6 +14,7 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
path_provider: ^2.1.2
|
||||
plugin_platform_interface: ^2.0.2
|
||||
|
||||
dev_dependencies:
|
||||
|
||||
Reference in New Issue
Block a user