feat(android): file sending improving
This commit is contained in:
+44
-30
@@ -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);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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 {
|
||||
///
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user