feat(android): implement file sending logic

This commit is contained in:
ksenia312
2024-02-01 17:27:00 +01:00
parent 5870dc2577
commit 46451ef447
22 changed files with 606 additions and 75 deletions
+2
View File
@@ -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';
+16
View File
@@ -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}';
}
}
+47
View File
@@ -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}';
}
}
+24 -16
View 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}';
}
}
+221
View File
@@ -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');
+21 -8
View File
@@ -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;
+3 -2
View File
@@ -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',
);
}
+15
View File
@@ -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;
}
}
+1 -1
View File
@@ -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,