feat(android): file sending improving

This commit is contained in:
ksenia312
2024-02-02 16:13:08 +01:00
parent 46451ef447
commit 466edeb39a
12 changed files with 590 additions and 221 deletions
+44 -30
View File
@@ -193,6 +193,7 @@ class AppService extends ChangeNotifier {
List<NearbyDevice>? 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<void> getPlatformInfo() async {
@@ -332,6 +332,7 @@ class AppService extends ChangeNotifier {
Future<void> 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<ReceivedNearbyMessage>? listener,
ValueChanged<File>? 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<bool?> 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<AppService>().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<AppService>().sendFileAccept(content);
}
});
},
);
}
@@ -907,7 +921,7 @@ class _ConnectedSocketBodyState extends State<_ConnectedSocketBody> {
child: _ActionButton(
title: 'Send',
onTap: () {
service.sendFile(filePath);
service.sendFileRequest(filePath);
},
),
),
+2 -2
View File
@@ -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 {
///
+125 -53
View File
@@ -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<String, dynamic>? 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<T>({
/// * The [onFileResponse] callback returns this instance of [NearbyMessageContent],
/// cast as [NearbyMessageFileResponse] if is a file response.
///
T? byType<T>({
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<String, dynamic>? 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<String, dynamic> 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<String, dynamic>? 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<String, dynamic>? json) {
return NearbyMessageFileResponse(
id: json?['id'] ?? '',
filePath: json?['filePath'] ?? '',
response: json?['response'] ?? false,
);
}
final bool response;
@override
bool get isValid => filePath.isNotEmpty;
@override
Map<String, dynamic> toJson() {
return {
'response': response,
...super.toJson(),
};
}
@override
String toString() {
return 'NearbyMessageFileResponse{response: $response, id: $id, filePath: $filePath}';
}
}
@@ -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)
///
@@ -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 = <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);
}
}
@@ -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 = <FilesSocket>[];
final _serverWaitingContents = <String, HttpRequest>{};
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<void> 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<void> closeAll() async {
for (final fileSocket in _filesSockets) {
await fileSocket.close();
}
_filesSockets.clear();
_filesListener = null;
}
Future<void> _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<void> _handleServerFileContent(
NearbyMessageFileContent content,
) async {
MapEntry<String, HttpRequest>? 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<void> _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;
}
}
@@ -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<bool> 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<bool> 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<void> _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<void> _startServerSubscription({
required NearbyServiceStreamListener socketListener,
required NearbyServiceMessagesListener socketListener,
required NearbyConnectionAndroidInfo info,
required int port,
ValueChanged<HttpRequest>? 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<int>) {
fileCreator!.add(event);
} else if (event == fileCreator?.finishCommand) {
final file = await fileCreator!.getFile().whenComplete(
() {
fileCreator = null;
},
// if (fileLoaders != null) {
// if (event is List<int>) {
// fileLoaders!.add(event);
// } else if (event == fileLoaders?.finishCommand) {
// final file = await fileLoaders!.getFile().whenComplete(
// () {
// fileLoaders = null;
// },
// );
// socketListener.onFile?.call(file);
// }
// } else {
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();
@@ -57,13 +57,21 @@ class NearbyServiceNetwork {
Future<WebSocket?> connectToSocket({
required String ownerIpAddress,
required int port,
required NearbySocketType socketType,
Map<String, String>? 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);
}
}
@@ -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);
}
+11 -5
View File
@@ -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}';
}
}
@@ -4,24 +4,58 @@ import 'package:nearby_service/nearby_service.dart';
///
/// Stream Subscription Listener.
///
class NearbyServiceStreamListener {
class NearbyServiceSocketListener<T> {
///
/// 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<ReceivedNearbyMessage> onMessage;
final ValueChanged<NearbyFile>? onFile;
final ValueChanged<T> onData;
final VoidCallback? onCreated;
final VoidCallback? onDone;
final void Function(Object, [StackTrace])? onError;
final bool? cancelOnError;
}
///
/// Stream Subscription Listener.
///
class NearbyServiceMessagesListener
extends NearbyServiceSocketListener<ReceivedNearbyMessage> {
///
/// 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<NearbyFile> {
///
/// 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,
});
}
+77
View File
@@ -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<int>) {
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>[];
int chunksCount = 0;
String get finishCommand => '$_finishCommand${content.id}';
void sendData(dynamic event) {
_socket.add(event);
}
void saveChunk(List<int> 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<NearbyFile> 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<void> close() => _socket.close();
}