Discover in different thread (#1555)

This commit is contained in:
Tien Do Nam
2024-07-24 03:34:53 +02:00
committed by GitHub
parent b70d3b813f
commit f5e8a5652c
135 changed files with 1706 additions and 1359 deletions
+25 -6
View File
@@ -19,16 +19,24 @@ jobs:
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: "stable"
- name: Dependencies
- name: Dependencies (app)
working-directory: app
run: flutter pub get
- name: Remove gen directory
- name: Remove gen directory (app)
working-directory: app
run: rm -rf lib/gen
- name: Check format
- name: Check format (app)
working-directory: app
run: dart format --line-length 150 --set-exit-if-changed lib test
- name: Dependencies (common)
working-directory: common
run: dart pub get
- name: Check format (common)
working-directory: common
run: dart format --line-length 150 --set-exit-if-changed lib test
test:
runs-on: ubuntu-latest
@@ -38,16 +46,27 @@ jobs:
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: "stable"
- name: Dependencies
- name: Dependencies (app)
working-directory: app
run: flutter pub get
- name: Analyze
- name: Analyze (app)
working-directory: app
run: flutter analyze
- name: Test
- name: Test (app)
working-directory: app
run: flutter test
- name: Dependencies (common)
working-directory: common
run: dart pub get
- name: Analyze (common)
working-directory: common
run: dart analyze
- name: Test (common)
working-directory: common
run: dart test
packaging:
runs-on: ubuntu-latest
+1
View File
@@ -1,5 +1,6 @@
## 1.15.2 (unreleased)
- feat: extract network scanning to separate threads, scanning should not cause UI lags anymore (@Tienisto)
- fix: memory leak when receiving files, properly receive files that exceed available RAM (@Tienisto)
- fix(android): save files outside Download folder (@Tienisto)
- fix(windows): make installer work on arm64 (@Tienisto)
+29 -6
View File
@@ -2,7 +2,13 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:common/common.dart';
import 'package:common/api_route_builder.dart';
import 'package:common/constants.dart';
import 'package:common/isolate.dart';
import 'package:common/model/dto/file_dto.dart';
import 'package:common/model/dto/multicast_dto.dart';
import 'package:common/util/dio.dart';
import 'package:common/util/logger.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
@@ -12,7 +18,6 @@ import 'package:localsend_app/pages/home_page_controller.dart';
import 'package:localsend_app/provider/animation_provider.dart';
import 'package:localsend_app/provider/app_arguments_provider.dart';
import 'package:localsend_app/provider/device_info_provider.dart';
import 'package:localsend_app/provider/dio_provider.dart';
import 'package:localsend_app/provider/network/nearby_devices_provider.dart';
import 'package:localsend_app/provider/network/server/server_provider.dart';
import 'package:localsend_app/provider/persistence_provider.dart';
@@ -22,13 +27,12 @@ import 'package:localsend_app/provider/purchase_provider.dart';
// [FOSS_REMOVE_END]
import 'package:localsend_app/provider/selection/selected_sending_files_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/provider/tv_provider.dart';
import 'package:localsend_app/provider/window_dimensions_provider.dart';
import 'package:localsend_app/refena.dart';
import 'package:localsend_app/theme.dart';
import 'package:localsend_app/util/api_route_builder.dart';
import 'package:localsend_app/util/i18n.dart';
import 'package:localsend_app/util/logger.dart';
import 'package:localsend_app/util/native/autostart_helper.dart';
import 'package:localsend_app/util/native/cache_helper.dart';
import 'package:localsend_app/util/native/context_menu_helper.dart';
@@ -120,8 +124,27 @@ Future<RefenaContainer> preInit(List<String> args) async {
],
);
// wait until all overrides are set
await container.ensureOverrides();
// initialize multi-threading
container.set(parentIsolateProvider.overrideWithNotifier((ref) {
final settings = ref.read(settingsProvider);
return ParentIsolateController(
initialState: ParentIsolateState.initial(
SyncState(
securityContext: persistenceService.getSecurityContext(),
deviceInfo: ref.read(deviceInfoProvider),
alias: settings.alias,
port: settings.port,
protocol: settings.https ? ProtocolType.https : ProtocolType.http,
multicastGroup: settings.multicastGroup,
discoveryTimeout: settings.discoveryTimeout,
serverRunning: true,
download: false,
),
),
);
}));
await container.redux(parentIsolateProvider).dispatchAsync(IsolateSetupAction());
return container;
}
+1 -1
View File
@@ -1,6 +1,6 @@
import 'dart:typed_data';
import 'package:common/common.dart';
import 'package:common/model/file_type.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
@@ -1,4 +1,4 @@
import 'package:common/common.dart';
import 'package:common/model/file_type.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:intl/intl.dart';
import 'package:localsend_app/gen/strings.g.dart';
@@ -1,4 +1,4 @@
import 'package:common/common.dart';
import 'package:common/model/device.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'nearby_devices_state.mapper.dart';
@@ -1,4 +1,5 @@
import 'package:common/common.dart';
import 'package:common/model/device.dart';
import 'package:common/model/session_status.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:dio/dio.dart';
import 'package:localsend_app/model/state/send/sending_file.dart';
+2 -1
View File
@@ -1,6 +1,7 @@
import 'dart:typed_data';
import 'package:common/common.dart';
import 'package:common/model/dto/file_dto.dart';
import 'package:common/model/file_status.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
@@ -1,4 +1,4 @@
import 'package:common/common.dart';
import 'package:common/model/dto/file_dto.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
@@ -1,6 +1,8 @@
import 'dart:async';
import 'package:common/common.dart';
import 'package:common/model/device.dart';
import 'package:common/model/file_type.dart';
import 'package:common/model/session_status.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/model/state/server/receiving_file.dart';
@@ -1,4 +1,5 @@
import 'package:common/common.dart';
import 'package:common/model/dto/file_dto.dart';
import 'package:common/model/file_status.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'receiving_file.mapper.dart';
+1 -1
View File
@@ -1,4 +1,4 @@
import 'package:common/common.dart';
import 'package:common/model/device.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/gen/strings.g.dart';
+1 -1
View File
@@ -1,4 +1,4 @@
import 'package:common/common.dart';
import 'package:common/model/file_type.dart';
import 'package:device_apps/device_apps.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/gen/strings.g.dart';
+3 -1
View File
@@ -1,7 +1,9 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:common/common.dart';
import 'package:common/model/dto/file_dto.dart';
import 'package:common/model/file_status.dart';
import 'package:common/model/session_status.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/gen/strings.g.dart';
+1 -1
View File
@@ -1,6 +1,6 @@
import 'dart:async';
import 'package:common/common.dart';
import 'package:common/model/session_status.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:localsend_app/gen/strings.g.dart';
+2 -1
View File
@@ -1,4 +1,5 @@
import 'package:common/common.dart';
import 'package:common/model/device.dart';
import 'package:common/model/session_status.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/model/persistence/receive_history_entry.dart';
import 'package:localsend_app/pages/progress_page.dart';
+1 -1
View File
@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:common/common.dart';
import 'package:common/model/file_type.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/provider/selection/selected_sending_files_provider.dart';
+2 -1
View File
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:common/common.dart';
import 'package:common/model/device.dart';
import 'package:common/model/session_status.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/provider/device_info_provider.dart';
+1 -1
View File
@@ -1,9 +1,9 @@
import 'package:common/util/sleep.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/model/state/server/server_state.dart';
import 'package:localsend_app/provider/local_ip_provider.dart';
import 'package:localsend_app/provider/network/server/server_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/util/sleep.dart';
import 'package:localsend_app/widget/dialogs/quick_save_notice.dart';
import 'package:refena_flutter/refena_flutter.dart';
+2 -1
View File
@@ -1,5 +1,6 @@
import 'package:collection/collection.dart';
import 'package:common/common.dart';
import 'package:common/model/device.dart';
import 'package:common/model/session_status.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/model/send_mode.dart';
+2 -1
View File
@@ -1,5 +1,6 @@
import 'package:collection/collection.dart';
import 'package:common/common.dart';
import 'package:common/model/device.dart';
import 'package:common/model/session_status.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/model/cross_file.dart';
import 'package:localsend_app/model/persistence/favorite_device.dart';
+2 -1
View File
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:common/common.dart';
import 'package:common/constants.dart';
import 'package:common/model/device.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/gen/strings.g.dart';
@@ -1,3 +1,5 @@
import 'package:common/model/device_info_result.dart';
import 'package:common/util/sleep.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/model/persistence/color_mode.dart';
import 'package:localsend_app/pages/language_page.dart';
@@ -8,8 +10,6 @@ import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/theme.dart';
import 'package:localsend_app/util/native/autostart_helper.dart';
import 'package:localsend_app/util/native/context_menu_helper.dart';
import 'package:localsend_app/util/native/device_info_helper.dart';
import 'package:localsend_app/util/sleep.dart';
import 'package:localsend_app/util/ui/dynamic_colors.dart';
import 'package:localsend_app/util/ui/snackbar.dart';
import 'package:refena_flutter/refena_flutter.dart';
+1 -1
View File
@@ -1,9 +1,9 @@
import 'package:common/model/device_info_result.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/model/persistence/color_mode.dart';
import 'package:localsend_app/model/state/server/server_state.dart';
import 'package:localsend_app/model/state/settings_state.dart';
import 'package:localsend_app/util/native/device_info_helper.dart';
part 'settings_tab_vm.mapper.dart';
+1 -1
View File
@@ -1,3 +1,4 @@
import 'package:common/util/sleep.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:localsend_app/gen/strings.g.dart';
@@ -7,7 +8,6 @@ import 'package:localsend_app/provider/network/server/server_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/theme.dart';
import 'package:localsend_app/util/native/platform_check.dart';
import 'package:localsend_app/util/sleep.dart';
import 'package:localsend_app/util/ui/snackbar.dart';
import 'package:localsend_app/widget/dialogs/pin_dialog.dart';
import 'package:localsend_app/widget/dialogs/qr_dialog.dart';
+6 -2
View File
@@ -1,10 +1,12 @@
import 'package:collection/collection.dart';
import 'package:common/common.dart';
import 'package:common/constants.dart';
import 'package:common/isolate.dart';
import 'package:common/model/device.dart';
import 'package:common/model/device_info_result.dart';
import 'package:localsend_app/provider/local_ip_provider.dart';
import 'package:localsend_app/provider/network/server/server_provider.dart';
import 'package:localsend_app/provider/security_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/util/native/device_info_helper.dart';
import 'package:refena_flutter/refena_flutter.dart';
final deviceRawInfoProvider = Provider<DeviceInfoResult>((ref) {
@@ -20,6 +22,8 @@ final deviceInfoProvider = ViewProvider<DeviceInfoResult>((ref) {
deviceModel: deviceModel ?? rawInfo.deviceModel,
androidSdkInt: rawInfo.androidSdkInt,
);
}, onChanged: (_, next, ref) {
ref.redux(parentIsolateProvider).dispatch(IsolateSyncDeviceInfoAction(deviceInfo: next));
});
final deviceFullInfoProvider = ViewProvider((ref) {
+16 -48
View File
@@ -1,8 +1,5 @@
import 'dart:io';
import 'package:common/common.dart';
import 'package:common/util/dio.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:localsend_app/provider/logging/http_logs_provider.dart';
import 'package:localsend_app/provider/security_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
@@ -19,53 +16,24 @@ class DioCollection {
}
/// Provides a dio having a specific timeout.
/// Changes must be made in common/lib/src/isolate/child/dio_provider.dart also
final dioProvider = ViewProvider((ref) {
final securityContext = ref.watch(securityProvider);
final discoveryTimeout = ref.watch(settingsProvider.select((state) => state.discoveryTimeout));
return DioCollection(
discovery: createDio(Duration(milliseconds: discoveryTimeout), securityContext, null),
longLiving: createDio(const Duration(days: 30), securityContext, ref),
);
});
/// It always trust the self signed certificate as all requests happen in a local network.
/// The user only needs to trust the local IP address.
/// Thanks to TCP (HTTP uses TCP), IP spoofing is nearly impossible.
Dio createDio(Duration timeout, StoredSecurityContext securityContext, [Ref? ref]) {
final dio = Dio(
BaseOptions(
connectTimeout: timeout,
sendTimeout: timeout,
discovery: createDio(Duration(milliseconds: discoveryTimeout), securityContext),
longLiving: createDio(
const Duration(days: 30),
securityContext,
interceptor: LogInterceptor(
requestHeader: false,
requestBody: true,
request: false,
responseHeader: false,
responseBody: true,
error: true,
logPrint: (log) => ref.notifier(httpLogsProvider).addLog(log.toString()),
),
),
);
// Allow any self signed certificate
dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient(
context: SecurityContext()
..usePrivateKeyBytes(securityContext.privateKey.codeUnits)
..useCertificateChainBytes(securityContext.certificate.codeUnits),
);
client.badCertificateCallback = (cert, host, port) => true;
return client;
},
);
// Add logging
if (ref != null) {
dio.interceptors.add(LogInterceptor(
requestHeader: false,
requestBody: true,
request: false,
responseHeader: false,
responseBody: true,
error: true,
logPrint: (log) {
ref.notifier(httpLogsProvider).addLog(log.toString());
},
));
}
return dio;
}
});
+1 -1
View File
@@ -1,4 +1,4 @@
import 'package:common/common.dart';
import 'package:common/model/device.dart';
import 'package:refena_flutter/refena_flutter.dart';
/// This provider stores the last devices that the user sent a file to.
@@ -1,194 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:common/common.dart';
import 'package:localsend_app/provider/device_info_provider.dart';
import 'package:localsend_app/provider/dio_provider.dart';
import 'package:localsend_app/provider/logging/discovery_logs_provider.dart';
import 'package:localsend_app/provider/network/server/server_provider.dart';
import 'package:localsend_app/provider/security_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/util/api_route_builder.dart';
import 'package:localsend_app/util/native/device_info_helper.dart';
import 'package:localsend_app/util/sleep.dart';
import 'package:logging/logging.dart';
import 'package:refena_flutter/refena_flutter.dart';
final _logger = Logger('Multicast');
final multicastProvider = ViewProvider((ref) {
final deviceInfo = ref.watch(deviceInfoProvider);
return MulticastService(ref, deviceInfo);
});
class MulticastService {
final Ref _ref;
final DeviceInfoResult _deviceInfo;
bool _listening = false;
MulticastService(this._ref, this._deviceInfo);
/// Binds the UDP port and listen to UDP multicast packages
/// It will automatically answer announcement messages
Stream<Device> startListener() async* {
if (_listening) {
_logger.info('Already listening to multicast');
return;
}
_listening = true;
final streamController = StreamController<Device>();
final settings = _ref.read(settingsProvider);
final fingerprint = _ref.read(securityProvider).certificateHash;
final sockets = await _getSockets(settings.multicastGroup, settings.port);
for (final socket in sockets) {
socket.socket.listen((_) {
final datagram = socket.socket.receive();
if (datagram == null) {
return;
}
try {
final dto = MulticastDto.fromJson(jsonDecode(utf8.decode(datagram.data)));
if (dto.fingerprint == fingerprint) {
return;
}
final ip = datagram.address.address;
final peer = dto.toDevice(ip, settings.port, settings.https);
streamController.add(peer);
if ((dto.announcement == true || dto.announce == true) && _ref.read(serverProvider) != null) {
// only respond when server is running
_answerAnnouncement(peer);
}
} catch (e) {
_ref.notifier(discoveryLoggerProvider).addLog(e.toString());
}
});
_ref.notifier(discoveryLoggerProvider).addLog(
'Bind UDP multicast port (ip: ${socket.interface.addresses.map((a) => a.address).toList()}, group: ${settings.multicastGroup}, port: ${settings.port})',
);
}
// Tell everyone in the network that I am online
unawaited(
sendAnnouncement(),
);
yield* streamController.stream;
}
/// Sends an announcement which triggers a response on every LocalSend member of the network.
Future<void> sendAnnouncement() async {
final settings = _ref.read(settingsProvider);
final sockets = await _getSockets(settings.multicastGroup);
final dto = _getMulticastDto(announcement: true);
for (final wait in [100, 500, 2000]) {
await sleepAsync(wait);
_ref.notifier(discoveryLoggerProvider).addLog('[ANNOUNCE/UDP]');
for (final socket in sockets) {
try {
socket.socket.send(dto, InternetAddress(settings.multicastGroup), settings.port);
socket.socket.close();
} catch (e) {
_ref.notifier(discoveryLoggerProvider).addLog(e.toString());
}
}
}
}
/// Responds to an announcement.
Future<void> _answerAnnouncement(Device peer) async {
final settings = _ref.read(settingsProvider);
try {
// Answer with TCP
await _ref.read(dioProvider).discovery.post(
ApiRoute.register.target(peer),
data: _getRegisterDto().toJson(),
);
_ref.notifier(discoveryLoggerProvider).addLog('[RESPONSE/TCP] Announcement of ${peer.alias} (${peer.ip}, model: ${peer.deviceModel}) via TCP');
} catch (e) {
// Fallback: Answer with UDP
final sockets = await _getSockets(settings.multicastGroup);
final dto = _getMulticastDto(announcement: false);
for (final socket in sockets) {
try {
socket.socket.send(dto, InternetAddress(settings.multicastGroup), settings.port);
socket.socket.close();
} catch (e) {
_ref.notifier(discoveryLoggerProvider).addLog(e.toString());
}
}
_ref
.notifier(discoveryLoggerProvider)
.addLog('[RESPONSE/UDP] Announcement of ${peer.alias} (${peer.ip}, model: ${peer.deviceModel}) with UDP because TCP failed');
}
}
/// Returns the MulticastDto of this device in bytes.
List<int> _getMulticastDto({required bool announcement}) {
final settings = _ref.read(settingsProvider);
final serverState = _ref.read(serverProvider);
final fingerprint = _ref.read(securityProvider).certificateHash;
final dto = MulticastDto(
alias: serverState?.alias ?? settings.alias,
version: protocolVersion,
deviceModel: _deviceInfo.deviceModel,
deviceType: _deviceInfo.deviceType,
fingerprint: fingerprint,
port: serverState?.port ?? settings.port,
protocol: (serverState?.https ?? settings.https) ? ProtocolType.https : ProtocolType.http,
download: serverState?.webSendState != null,
announcement: announcement,
announce: announcement,
);
return utf8.encode(jsonEncode(dto.toJson()));
}
RegisterDto _getRegisterDto() {
final settings = _ref.read(settingsProvider);
final serverState = _ref.read(serverProvider);
final fingerprint = _ref.read(securityProvider).certificateHash;
return RegisterDto(
alias: serverState?.alias ?? settings.alias,
version: protocolVersion,
deviceModel: _deviceInfo.deviceModel,
deviceType: _deviceInfo.deviceType,
fingerprint: fingerprint,
port: serverState?.port ?? settings.port,
protocol: (serverState?.https ?? settings.https) ? ProtocolType.https : ProtocolType.http,
download: serverState?.webSendState != null,
);
}
}
class _SocketResult {
final NetworkInterface interface;
final RawDatagramSocket socket;
_SocketResult(this.interface, this.socket);
}
Future<List<_SocketResult>> _getSockets(String multicastGroup, [int? port]) async {
final interfaces = await NetworkInterface.list();
final sockets = <_SocketResult>[];
for (final interface in interfaces) {
try {
final socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, port ?? 0);
socket.joinMulticast(InternetAddress(multicastGroup), interface);
sockets.add(_SocketResult(interface, socket));
} catch (e) {
_logger.warning(
'Could not bind UDP multicast port (ip: ${interface.addresses.map((a) => a.address).toList()}, group: $multicastGroup, port: $port)',
e,
);
}
}
return sockets;
}
@@ -1,19 +1,14 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:common/common.dart';
import 'package:common/isolate.dart';
import 'package:common/model/device.dart';
import 'package:localsend_app/model/persistence/favorite_device.dart';
import 'package:localsend_app/model/state/nearby_devices_state.dart';
import 'package:localsend_app/provider/favorites_provider.dart';
import 'package:localsend_app/provider/logging/discovery_logs_provider.dart';
import 'package:localsend_app/provider/network/multicast_provider.dart';
import 'package:localsend_app/provider/network/targeted_discovery_provider.dart';
import 'package:localsend_app/util/task_runner.dart';
import 'package:logging/logging.dart';
import 'package:refena_flutter/refena_flutter.dart';
final _logger = Logger('NearbyDevices');
/// This provider is responsible for:
/// - Scanning the network for other LocalSend instances
/// - Keeping track of all found devices (they are only stored in RAM)
@@ -22,28 +17,22 @@ final _logger = Logger('NearbyDevices');
final nearbyDevicesProvider = ReduxProvider<NearbyDevicesService, NearbyDevicesState>((ref) {
return NearbyDevicesService(
discoveryLogs: ref.notifier(discoveryLoggerProvider),
targetedDiscoveryService: ref.accessor(targetedDiscoveryProvider),
multicastService: ref.accessor(multicastProvider),
isolateController: ref.notifier(parentIsolateProvider),
favoriteService: ref.notifier(favoritesProvider),
);
});
Map<String, TaskRunner> _runners = {};
class NearbyDevicesService extends ReduxNotifier<NearbyDevicesState> {
final DiscoveryLogger _discoveryLogs;
final StateAccessor<TargetedDiscoveryService> _targetedDiscoveryService;
final StateAccessor<MulticastService> _multicastService;
final ParentIsolateController _isolateController;
final FavoritesService _favoriteService;
NearbyDevicesService({
required DiscoveryLogger discoveryLogs,
required StateAccessor<TargetedDiscoveryService> targetedDiscoveryService,
required StateAccessor<MulticastService> multicastService,
required ParentIsolateController isolateController,
required FavoritesService favoriteService,
}) : _discoveryLogs = discoveryLogs,
_targetedDiscoveryService = targetedDiscoveryService,
_multicastService = multicastService,
_isolateController = isolateController,
_favoriteService = favoriteService;
@override
@@ -52,50 +41,6 @@ class NearbyDevicesService extends ReduxNotifier<NearbyDevicesState> {
runningIps: {},
devices: {},
);
Stream<Device> _getStream(String networkInterface, int port, bool https) {
final ipList = List.generate(256, (i) => '${networkInterface.split('.').take(3).join('.')}.$i').where((ip) => ip != networkInterface).toList();
_runners[networkInterface]?.stop();
_runners[networkInterface] = TaskRunner<Device?>(
initialTasks: List.generate(
ipList.length,
(index) => () async => _doRequest(ipList[index], port, https),
),
concurrency: 50,
);
return _runners[networkInterface]!.stream.where((device) => device != null).cast<Device>();
}
Stream<Device> _getFavoriteStream({required List<FavoriteDevice> devices, required bool https}) {
final runner = TaskRunner<Device?>(
initialTasks: List.generate(
devices.length,
(index) => () async {
final device = devices[index];
return _doRequest(device.ip, device.port, https);
},
),
concurrency: 50,
);
return runner.stream.where((device) => device != null).cast<Device>();
}
Future<Device?> _doRequest(String currentIp, int port, bool https) async {
_logger.fine('Requesting $currentIp');
final device = await _targetedDiscoveryService.state.discover(
ip: currentIp,
port: port,
https: https,
onError: null,
);
if (device != null) {
_discoveryLogs.addLog('[DISCOVER/TCP] ${device.alias} (${device.ip}, model: ${device.deviceModel})');
}
return device;
}
}
/// Binds the UDP port and listens for incoming announcements.
@@ -103,7 +48,7 @@ class NearbyDevicesService extends ReduxNotifier<NearbyDevicesState> {
class StartMulticastListener extends AsyncReduxAction<NearbyDevicesService, NearbyDevicesState> {
@override
Future<NearbyDevicesState> reduce() async {
await for (final device in notifier._multicastService.state.startListener()) {
await for (final device in notifier._isolateController.state.multicastDiscovery!.receiveFromIsolate) {
await dispatchAsync(RegisterDeviceAction(device));
notifier._discoveryLogs.addLog('[DISCOVER/UDP] ${device.alias} (${device.ip}, model: ${device.deviceModel})');
}
@@ -149,7 +94,7 @@ class RegisterDeviceAction extends AsyncReduxAction<NearbyDevicesService, Nearby
class StartMulticastScan extends ReduxAction<NearbyDevicesService, NearbyDevicesState> {
@override
NearbyDevicesState reduce() {
notifier._multicastService.state.sendAnnouncement(); // ignore: discarded_futures
external(notifier._isolateController).dispatch(IsolateSendMulticastAnnouncementAction());
return state;
}
}
@@ -176,7 +121,14 @@ class StartLegacyScan extends AsyncReduxAction<NearbyDevicesService, NearbyDevic
dispatch(_SetRunningIpsAction({...state.runningIps, localIp}));
await for (final device in notifier._getStream(localIp, port, https)) {
final stream = external(notifier._isolateController).dispatchTakeResult(IsolateInterfaceHttpDiscoveryAction(
networkInterface: localIp,
port: port,
https: https,
));
await for (final device in stream) {
notifier._discoveryLogs.addLog('[DISCOVER/TCP] ${device.alias} (${device.ip}, model: ${device.deviceModel})');
await dispatchAsync(RegisterDeviceAction(device));
}
@@ -201,9 +153,17 @@ class StartFavoriteScan extends AsyncReduxAction<NearbyDevicesService, NearbyDev
return state;
}
dispatch(_SetRunningFavoriteScanAction(true));
await for (final device in notifier._getFavoriteStream(devices: devices, https: https)) {
final stream = external(notifier._isolateController).dispatchTakeResult(IsolateFavoriteHttpDiscoveryAction(
favorites: devices.map((e) => (e.ip, e.port)).toList(),
https: https,
));
await for (final device in stream) {
notifier._discoveryLogs.addLog('[DISCOVER/TCP] ${device.alias} (${device.ip}, model: ${device.deviceModel})');
await dispatchAsync(RegisterDeviceAction(device));
}
return state.copyWith(
runningFavoriteScan: false,
);
+1 -1
View File
@@ -1,10 +1,10 @@
import 'dart:async';
import 'package:common/util/sleep.dart';
import 'package:localsend_app/provider/favorites_provider.dart';
import 'package:localsend_app/provider/local_ip_provider.dart';
import 'package:localsend_app/provider/network/nearby_devices_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/util/sleep.dart';
import 'package:refena_flutter/refena_flutter.dart';
/// Scans the network via multicast first,
+11 -3
View File
@@ -2,7 +2,17 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:common/common.dart';
import 'package:common/api_route_builder.dart';
import 'package:common/model/device.dart';
import 'package:common/model/dto/file_dto.dart';
import 'package:common/model/dto/info_register_dto.dart';
import 'package:common/model/dto/multicast_dto.dart';
import 'package:common/model/dto/prepare_upload_request_dto.dart';
import 'package:common/model/dto/prepare_upload_response_dto.dart';
import 'package:common/model/file_status.dart';
import 'package:common/model/file_type.dart';
import 'package:common/model/session_status.dart';
import 'package:common/util/sleep.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/model/cross_file.dart';
@@ -17,8 +27,6 @@ import 'package:localsend_app/provider/dio_provider.dart';
import 'package:localsend_app/provider/progress_provider.dart';
import 'package:localsend_app/provider/selection/selected_sending_files_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/util/api_route_builder.dart';
import 'package:localsend_app/util/sleep.dart';
import 'package:localsend_app/widget/dialogs/pin_dialog.dart';
import 'package:logging/logging.dart';
import 'package:refena_flutter/refena_flutter.dart';
@@ -2,7 +2,16 @@ import 'dart:async';
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:common/common.dart';
import 'package:common/api_route_builder.dart';
import 'package:common/constants.dart';
import 'package:common/model/dto/info_dto.dart';
import 'package:common/model/dto/info_register_dto.dart';
import 'package:common/model/dto/prepare_upload_request_dto.dart';
import 'package:common/model/dto/prepare_upload_response_dto.dart';
import 'package:common/model/dto/register_dto.dart';
import 'package:common/model/file_status.dart';
import 'package:common/model/file_type.dart';
import 'package:common/model/session_status.dart';
import 'package:flutter/foundation.dart';
import 'package:localsend_app/model/state/send/send_session_state.dart';
import 'package:localsend_app/model/state/server/receive_session_state.dart';
@@ -25,7 +34,6 @@ import 'package:localsend_app/provider/receive_history_provider.dart';
import 'package:localsend_app/provider/selection/selected_receiving_files_provider.dart';
import 'package:localsend_app/provider/selection/selected_sending_files_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/util/api_route_builder.dart';
import 'package:localsend_app/util/native/directories.dart';
import 'package:localsend_app/util/native/file_saver.dart';
import 'package:localsend_app/util/native/platform_check.dart';
@@ -2,7 +2,12 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:common/common.dart';
import 'package:common/api_route_builder.dart';
import 'package:common/constants.dart';
import 'package:common/model/dto/file_dto.dart';
import 'package:common/model/dto/info_dto.dart';
import 'package:common/model/dto/receive_request_response_dto.dart';
import 'package:common/model/file_type.dart';
import 'package:localsend_app/gen/assets.gen.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/model/cross_file.dart';
@@ -13,7 +18,6 @@ import 'package:localsend_app/provider/device_info_provider.dart';
import 'package:localsend_app/provider/network/server/controller/common.dart';
import 'package:localsend_app/provider/network/server/server_utils.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/util/api_route_builder.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:uri_content/uri_content.dart';
@@ -1,7 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'package:common/common.dart';
import 'package:common/constants.dart';
import 'package:common/isolate.dart';
import 'package:common/model/dto/multicast_dto.dart';
import 'package:localsend_app/model/cross_file.dart';
import 'package:localsend_app/model/state/server/server_state.dart';
import 'package:localsend_app/provider/network/server/controller/receive_controller.dart';
@@ -23,6 +25,29 @@ final _logger = Logger('Server');
/// The server can receive files (since v1) and send files (since v2).
final serverProvider = NotifierProvider<ServerService, ServerState?>((ref) {
return ServerService();
}, onChanged: (_, next, ref) {
final settings = ref.read(settingsProvider);
final syncState = ref.read(parentIsolateProvider).syncState;
final syncStatePrev = (syncState.alias, syncState.port, syncState.protocol, syncState.serverRunning, syncState.download);
final syncStateNext = (
next?.alias ?? settings.alias,
next?.port ?? settings.port,
(next?.https ?? settings.https) ? ProtocolType.https : ProtocolType.http,
next != null,
next?.webSendState != null,
);
if (syncStatePrev == syncStateNext) {
return;
}
ref.redux(parentIsolateProvider).dispatch(IsolateSyncServerStateAction(
alias: syncStateNext.$1,
port: syncStateNext.$2,
protocol: syncStateNext.$3,
serverRunning: syncStateNext.$4,
download: syncStateNext.$5,
));
});
class ServerService extends Notifier<ServerState?> {
+3 -1
View File
@@ -2,7 +2,9 @@ import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:common/common.dart';
import 'package:common/constants.dart';
import 'package:common/model/device.dart';
import 'package:common/model/stored_security_context.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/gen/strings.g.dart';
@@ -1,4 +1,4 @@
import 'package:common/common.dart';
import 'package:common/model/file_type.dart';
import 'package:localsend_app/model/persistence/receive_history_entry.dart';
import 'package:localsend_app/provider/persistence_provider.dart';
import 'package:refena_flutter/refena_flutter.dart';
+4 -1
View File
@@ -1,4 +1,5 @@
import 'package:common/common.dart';
import 'package:common/isolate.dart';
import 'package:common/model/stored_security_context.dart';
import 'package:localsend_app/provider/persistence_provider.dart';
import 'package:localsend_app/util/security_helper.dart';
import 'package:refena_flutter/refena_flutter.dart';
@@ -7,6 +8,8 @@ import 'package:refena_flutter/refena_flutter.dart';
/// It contains all the security related data for HTTPS communication.
final securityProvider = ReduxProvider<SecurityService, StoredSecurityContext>((ref) {
return SecurityService(ref.read(persistenceProvider));
}, onChanged: (_, next, ref) {
ref.redux(parentIsolateProvider).dispatch(IsolateSyncSecurityContextAction(securityContext: next));
});
class SecurityService extends ReduxNotifier<StoredSecurityContext> {
@@ -1,5 +1,5 @@
import 'package:collection/collection.dart';
import 'package:common/common.dart';
import 'package:common/model/dto/file_dto.dart';
import 'package:localsend_app/util/file_path_helper.dart';
import 'package:refena_flutter/refena_flutter.dart';
import 'package:uuid/uuid.dart';
@@ -2,7 +2,7 @@ import 'dart:convert' show utf8;
import 'dart:io';
import 'dart:typed_data';
import 'package:common/common.dart';
import 'package:common/model/file_type.dart';
import 'package:localsend_app/model/cross_file.dart';
import 'package:localsend_app/util/file_path_helper.dart';
import 'package:localsend_app/util/native/cache_helper.dart';
+12 -1
View File
@@ -1,4 +1,5 @@
import 'package:common/common.dart';
import 'package:common/isolate.dart';
import 'package:common/model/device.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/model/persistence/color_mode.dart';
@@ -9,6 +10,16 @@ import 'package:refena_flutter/refena_flutter.dart';
final settingsProvider = NotifierProvider<SettingsService, SettingsState>((ref) {
return SettingsService(ref.read(persistenceProvider));
}, onChanged: (_, next, ref) {
final syncState = ref.read(parentIsolateProvider).syncState;
if (syncState.multicastGroup == next.multicastGroup && syncState.discoveryTimeout == next.discoveryTimeout) {
return;
}
ref.redux(parentIsolateProvider).dispatch(IsolateSyncSettingsAction(
multicastGroup: next.multicastGroup,
discoveryTimeout: next.discoveryTimeout,
));
});
class SettingsService extends PureNotifier<SettingsState> {
+1 -1
View File
@@ -1,4 +1,4 @@
import 'package:common/common.dart';
import 'package:common/model/device.dart';
import 'package:flutter/material.dart';
extension DeviceTypeExt on DeviceType {
+1 -1
View File
@@ -1,5 +1,5 @@
import 'package:collection/collection.dart';
import 'package:common/common.dart';
import 'package:common/model/device.dart';
import 'package:localsend_app/model/persistence/favorite_device.dart';
extension FavoriteDevicesExt on Iterable<FavoriteDevice> {
+1 -1
View File
@@ -1,4 +1,4 @@
import 'package:common/common.dart';
import 'package:common/model/file_type.dart';
/// Matches myFile-123 -> 123
final _fileNumberRegex = RegExp(r'^(.*)(?:-(\d+))$');
+1 -1
View File
@@ -1,4 +1,4 @@
import 'package:common/common.dart';
import 'package:common/model/file_type.dart';
import 'package:flutter/material.dart';
extension FileTypeExt on FileType {
+1 -1
View File
@@ -4,10 +4,10 @@ import 'dart:io';
import 'dart:isolate';
import 'package:app_group_directory/app_group_directory.dart';
import 'package:common/util/logger.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.dart';
import 'package:localsend_app/util/file_path_helper.dart';
import 'package:localsend_app/util/logger.dart';
import 'package:localsend_app/util/native/platform_check.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
@@ -1,6 +1,6 @@
import 'dart:io';
import 'package:common/common.dart';
import 'package:common/model/file_type.dart';
import 'package:device_apps/device_apps.dart';
import 'package:flutter/foundation.dart';
import 'package:image_picker/image_picker.dart';
+2 -16
View File
@@ -1,4 +1,5 @@
import 'package:common/common.dart';
import 'package:common/model/device.dart';
import 'package:common/model/device_info_result.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:slang/builder/model/enums.dart';
@@ -6,21 +7,6 @@ import 'package:slang/builder/model/enums.dart';
// ignore: implementation_imports
import 'package:slang/src/builder/utils/string_extensions.dart';
class DeviceInfoResult {
final DeviceType deviceType;
final String? deviceModel;
// Used to properly set Edge-to-Edge mode on Android
// See https://github.com/flutter/flutter/issues/90098
final int? androidSdkInt;
DeviceInfoResult({
required this.deviceType,
required this.deviceModel,
required this.androidSdkInt,
});
}
Future<DeviceInfoResult> getDeviceInfo() async {
final plugin = DeviceInfoPlugin();
final DeviceType deviceType;
+3 -3
View File
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:common/common.dart';
import 'package:common/model/file_type.dart';
import 'package:common/util/sleep.dart';
import 'package:file_selector/file_selector.dart' as file_selector;
import 'package:file_selector/file_selector.dart';
import 'package:flutter/foundation.dart';
@@ -16,7 +17,6 @@ import 'package:localsend_app/util/native/cross_file_converters.dart';
import 'package:localsend_app/util/native/file_picker_android.dart';
import 'package:localsend_app/util/native/pick_directory_path.dart';
import 'package:localsend_app/util/native/platform_check.dart';
import 'package:localsend_app/util/sleep.dart';
import 'package:localsend_app/util/ui/asset_picker_translated_text_delegate.dart';
import 'package:localsend_app/widget/dialogs/loading_dialog.dart';
import 'package:localsend_app/widget/dialogs/message_input_dialog.dart';
@@ -199,7 +199,7 @@ Future<void> _pickFolder(BuildContext context, Ref ref) async {
);
await sleepAsync(200); // Wait for the dialog to be shown
try {
if (defaultTargetPlatform == TargetPlatform.android && (ref.read(deviceRawInfoProvider).androidSdkInt ?? 0) >= contentUriMinSdk) {
if (defaultTargetPlatform == TargetPlatform.android && (ref.read(deviceInfoProvider).androidSdkInt ?? 0) >= contentUriMinSdk) {
// Android 8 and above have more predictable content URIs that we can parse.
final result = await pickDirectoryAndroid();
if (result != null) {
+1 -1
View File
@@ -1,4 +1,4 @@
import 'package:common/common.dart';
import 'package:common/model/file_type.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/util/native/platform_check.dart';
import 'package:localsend_app/widget/dialogs/cannot_open_file_dialog.dart';
+1 -1
View File
@@ -2,7 +2,7 @@ import 'dart:convert';
import 'dart:typed_data';
import 'package:basic_utils/basic_utils.dart';
import 'package:common/common.dart';
import 'package:common/model/stored_security_context.dart';
/// Generates a random [SecurityContextResult].
StoredSecurityContext generateSecurityContext([AsymmetricKeyPair? keyPair]) {
@@ -1,5 +1,5 @@
import 'package:common/util/sleep.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/util/sleep.dart';
/// The same as AnimatedCrossFade but with an opacity animation
class AnimatedOpacityCrossFade extends StatefulWidget {
@@ -1,5 +1,5 @@
import 'package:common/util/sleep.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/util/sleep.dart';
class InitialFadeTransition extends StatefulWidget {
final Widget child;
@@ -1,5 +1,5 @@
import 'package:common/util/sleep.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/util/sleep.dart';
class InitialSlideTransition extends StatefulWidget {
final Widget child;
@@ -1,14 +1,14 @@
import 'package:collection/collection.dart';
import 'package:common/common.dart';
import 'package:common/isolate.dart';
import 'package:common/model/device.dart';
import 'package:common/util/task_runner.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/provider/last_devices.provider.dart';
import 'package:localsend_app/provider/local_ip_provider.dart';
import 'package:localsend_app/provider/network/targeted_discovery_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/theme.dart';
import 'package:localsend_app/util/task_runner.dart';
import 'package:refena_flutter/refena_flutter.dart';
import 'package:routerino/routerino.dart';
@@ -59,7 +59,12 @@ class _AddressInputDialogState extends State<AddressInputDialog> with Refena {
final results = TaskRunner<Device?>(
concurrency: 10,
initialTasks: [
for (final ip in candidates) () => ref.read(targetedDiscoveryProvider).discover(ip: ip, port: port, https: https),
for (final ip in candidates)
() => ref.redux(parentIsolateProvider).dispatchAsyncTakeResult(IsolateTargetHttpDiscoveryAction(
ip: ip,
port: port,
https: https,
)),
],
).stream;
+6 -2
View File
@@ -1,8 +1,8 @@
import 'package:common/isolate.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/model/persistence/favorite_device.dart';
import 'package:localsend_app/provider/favorites_provider.dart';
import 'package:localsend_app/provider/network/targeted_discovery_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/theme.dart';
import 'package:localsend_app/widget/dialogs/favorite_edit_dialog.dart';
@@ -29,7 +29,11 @@ class _FavoritesDialogState extends State<FavoritesDialog> with Refena {
final https = ref.read(settingsProvider).https;
final result = await ref.read(targetedDiscoveryProvider).discover(ip: favorite.ip, port: favorite.port, https: https);
final result = await ref.redux(parentIsolateProvider).dispatchAsyncTakeResult(IsolateTargetHttpDiscoveryAction(
ip: favorite.ip,
port: favorite.port,
https: https,
));
if (result == null) {
setState(() {
_fetching = false;
@@ -1,9 +1,9 @@
import 'package:common/common.dart';
import 'package:common/isolate.dart';
import 'package:common/model/device.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/model/persistence/favorite_device.dart';
import 'package:localsend_app/provider/favorites_provider.dart';
import 'package:localsend_app/provider/network/targeted_discovery_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/theme.dart';
import 'package:localsend_app/widget/dialogs/favorite_delete_dialog.dart';
@@ -157,7 +157,11 @@ class _FavoriteEditDialogState extends State<FavoriteEditDialog> with Refena {
setState(() {
_fetching = true;
});
final result = await ref.read(targetedDiscoveryProvider).discover(ip: ip, port: port, https: https);
final result = await ref.redux(parentIsolateProvider).dispatchAsyncTakeResult(IsolateTargetHttpDiscoveryAction(
ip: ip,
port: port,
https: https,
));
if (result == null) {
setState(() {
_fetching = false;
+1 -1
View File
@@ -1,7 +1,7 @@
import 'dart:io';
import 'dart:ui';
import 'package:common/common.dart';
import 'package:common/model/file_type.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/model/cross_file.dart';
@@ -1,4 +1,4 @@
import 'package:common/common.dart';
import 'package:common/model/device.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/util/device_type_ext.dart';
import 'package:localsend_app/util/ip_helper.dart';
@@ -1,4 +1,4 @@
import 'package:common/common.dart';
import 'package:common/model/device.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/provider/animation_provider.dart';
import 'package:localsend_app/util/device_type_ext.dart';
+1 -1
View File
@@ -1,7 +1,7 @@
import 'dart:async';
import 'package:common/util/sleep.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/util/sleep.dart';
/// A slideshow of widgets using [AnimatedOpacity] as transition.
class OpacitySlideshow extends StatefulWidget {
+1 -1
View File
@@ -1,5 +1,5 @@
import 'package:common/util/sleep.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/util/sleep.dart';
class RotatingWidget extends StatefulWidget {
final Duration duration;
+5 -4
View File
@@ -5,7 +5,8 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i4;
import 'package:common/common.dart' as _i2;
import 'package:common/model/device.dart' as _i12;
import 'package:common/model/stored_security_context.dart' as _i2;
import 'package:flutter/material.dart' as _i8;
import 'package:localsend_app/gen/strings.g.dart' as _i10;
import 'package:localsend_app/model/persistence/color_mode.dart' as _i9;
@@ -15,7 +16,7 @@ import 'package:localsend_app/model/send_mode.dart' as _i11;
import 'package:localsend_app/provider/persistence_provider.dart' as _i3;
import 'package:mockito/mockito.dart' as _i1;
import 'package:mockito/src/dummies.dart' as _i7;
import 'package:shared_preferences/shared_preferences.dart' as _i12;
import 'package:shared_preferences/shared_preferences.dart' as _i13;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
@@ -570,7 +571,7 @@ class MockPersistenceService extends _i1.Mock implements _i3.PersistenceService
) as bool);
@override
_i4.Future<void> setDeviceType(_i2.DeviceType? deviceType) => (super.noSuchMethod(
_i4.Future<void> setDeviceType(_i12.DeviceType? deviceType) => (super.noSuchMethod(
Invocation.method(
#setDeviceType,
[deviceType],
@@ -603,7 +604,7 @@ class MockPersistenceService extends _i1.Mock implements _i3.PersistenceService
/// A class which mocks [SharedPreferences].
///
/// See the documentation for Mockito's code generation for more information.
class MockSharedPreferences extends _i1.Mock implements _i12.SharedPreferences {
class MockSharedPreferences extends _i1.Mock implements _i13.SharedPreferences {
@override
Set<String> getKeys() => (super.noSuchMethod(
Invocation.method(
@@ -1,4 +1,10 @@
import 'package:common/common.dart';
import 'package:common/model/device.dart';
import 'package:common/model/dto/file_dto.dart';
import 'package:common/model/dto/info_register_dto.dart';
import 'package:common/model/dto/multicast_dto.dart';
import 'package:common/model/dto/prepare_upload_request_dto.dart';
import 'package:common/model/dto/prepare_upload_response_dto.dart';
import 'package:common/model/file_type.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:test/test.dart';
@@ -1,4 +1,4 @@
import 'package:common/common.dart';
import 'package:common/model/device.dart';
import 'package:localsend_app/provider/last_devices.provider.dart';
import 'package:refena_flutter/refena_flutter.dart';
import 'package:test/test.dart';
@@ -1,4 +1,4 @@
import 'package:common/common.dart';
import 'package:common/model/file_type.dart';
import 'package:localsend_app/model/persistence/receive_history_entry.dart';
import 'package:localsend_app/provider/receive_history_provider.dart';
import 'package:mockito/mockito.dart';
@@ -1,5 +1,5 @@
import 'package:common/common.dart';
import 'package:localsend_app/util/api_route_builder.dart';
import 'package:common/api_route_builder.dart';
import 'package:common/model/device.dart';
import 'package:test/test.dart';
void main() {
@@ -1,4 +1,4 @@
import 'package:common/common.dart';
import 'package:common/model/device.dart';
const _basePath = '/api/localsend';
-14
View File
@@ -1,14 +0,0 @@
export 'package:common/src/constants.dart';
export 'package:common/src/model/device.dart';
export 'package:common/src/model/dto/file_dto.dart';
export 'package:common/src/model/dto/info_dto.dart';
export 'package:common/src/model/dto/info_register_dto.dart';
export 'package:common/src/model/dto/multicast_dto.dart';
export 'package:common/src/model/dto/prepare_upload_request_dto.dart';
export 'package:common/src/model/dto/prepare_upload_response_dto.dart';
export 'package:common/src/model/dto/receive_request_response_dto.dart';
export 'package:common/src/model/dto/register_dto.dart';
export 'package:common/src/model/file_status.dart';
export 'package:common/src/model/file_type.dart';
export 'package:common/src/model/session_status.dart';
export 'package:common/src/model/stored_security_context.dart';
+4
View File
@@ -0,0 +1,4 @@
export 'package:common/src/isolate/child/sync_provider.dart';
export 'package:common/src/isolate/parent/actions.dart';
export 'package:common/src/isolate/parent/parent_isolate_provider.dart';
export 'package:common/src/isolate/parent/sync_actions.dart';
+16
View File
@@ -0,0 +1,16 @@
import 'package:common/model/device.dart';
class DeviceInfoResult {
final DeviceType deviceType;
final String? deviceModel;
// Used to properly set Edge-to-Edge mode on Android
// See https://github.com/flutter/flutter/issues/90098
final int? androidSdkInt;
DeviceInfoResult({
required this.deviceType,
required this.deviceModel,
required this.androidSdkInt,
});
}
@@ -1,5 +1,5 @@
import 'package:collection/collection.dart';
import 'package:common/src/model/file_type.dart';
import 'package:common/model/file_type.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:mime/mime.dart';
@@ -1,5 +1,5 @@
import 'package:common/src/constants.dart';
import 'package:common/src/model/device.dart';
import 'package:common/constants.dart';
import 'package:common/model/device.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'info_dto.mapper.dart';
@@ -1,6 +1,6 @@
import 'package:common/src/constants.dart';
import 'package:common/src/model/device.dart';
import 'package:common/src/model/dto/multicast_dto.dart';
import 'package:common/constants.dart';
import 'package:common/model/device.dart';
import 'package:common/model/dto/multicast_dto.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'info_register_dto.mapper.dart';
@@ -1,4 +1,5 @@
import 'package:common/common.dart';
import 'package:common/constants.dart';
import 'package:common/model/device.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'multicast_dto.mapper.dart';
@@ -1,5 +1,5 @@
import 'package:common/src/model/dto/file_dto.dart';
import 'package:common/src/model/dto/info_register_dto.dart';
import 'package:common/model/dto/file_dto.dart';
import 'package:common/model/dto/info_register_dto.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'prepare_upload_request_dto.mapper.dart';
@@ -1,5 +1,5 @@
import 'package:common/src/model/dto/file_dto.dart';
import 'package:common/src/model/dto/info_dto.dart';
import 'package:common/model/dto/file_dto.dart';
import 'package:common/model/dto/info_dto.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'receive_request_response_dto.mapper.dart';
@@ -1,6 +1,6 @@
import 'package:common/src/constants.dart';
import 'package:common/src/model/device.dart';
import 'package:common/src/model/dto/multicast_dto.dart';
import 'package:common/constants.dart';
import 'package:common/model/device.dart';
import 'package:common/model/dto/multicast_dto.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'register_dto.mapper.dart';
@@ -0,0 +1,67 @@
import 'package:common/model/device.dart';
import 'package:common/src/discovery/http_target_discovery.dart';
import 'package:common/util/task_runner.dart';
import 'package:logging/logging.dart';
import 'package:refena/refena.dart';
final _logger = Logger('HttpScanDiscovery');
final httpScanDiscoveryProvider = ViewProvider((ref) {
return HttpScanDiscoveryService(
targetedDiscoveryService: ref.accessor(httpTargetDiscoveryProvider),
);
});
Map<String, TaskRunner> _runners = {};
class HttpScanDiscoveryService {
final StateAccessor<HttpTargetDiscoveryService> _targetedDiscoveryService;
HttpScanDiscoveryService({
required StateAccessor<HttpTargetDiscoveryService> targetedDiscoveryService,
}) : _targetedDiscoveryService = targetedDiscoveryService;
Stream<Device> getStream({required String networkInterface, required int port, required bool https}) {
final ipList = List.generate(256, (i) => '${networkInterface.split('.').take(3).join('.')}.$i').where((ip) => ip != networkInterface).toList();
_runners[networkInterface]?.stop();
_runners[networkInterface] = TaskRunner<Device?>(
initialTasks: List.generate(
ipList.length,
(index) => () async => _doRequest(ipList[index], port, https),
),
concurrency: 50,
);
return _runners[networkInterface]!.stream.where((device) => device != null).cast<Device>();
}
Stream<Device> getFavoriteStream({required List<(String, int)> devices, required bool https}) {
final runner = TaskRunner<Device?>(
initialTasks: List.generate(
devices.length,
(index) => () async {
final device = devices[index];
return _doRequest(device.$1, device.$2, https);
},
),
concurrency: 50,
);
return runner.stream.where((device) => device != null).cast<Device>();
}
Future<Device?> _doRequest(String currentIp, int port, bool https) async {
_logger.fine('Requesting $currentIp');
final device = await _targetedDiscoveryService.state.discover(
ip: currentIp,
port: port,
https: https,
onError: null,
);
if (device != null) {
_logger.info('[DISCOVER/TCP] ${device.alias} (${device.ip}, model: ${device.deviceModel})');
}
return device;
}
}
@@ -1,25 +1,27 @@
import 'package:common/common.dart';
import 'package:common/api_route_builder.dart';
import 'package:common/constants.dart';
import 'package:common/model/device.dart';
import 'package:common/model/dto/info_dto.dart';
import 'package:common/src/isolate/child/dio_provider.dart';
import 'package:common/src/isolate/child/sync_provider.dart';
import 'package:dio/dio.dart';
import 'package:localsend_app/provider/dio_provider.dart';
import 'package:localsend_app/provider/security_provider.dart';
import 'package:localsend_app/util/api_route_builder.dart';
import 'package:logging/logging.dart';
import 'package:refena_flutter/refena_flutter.dart';
import 'package:refena/refena.dart';
final _logger = Logger('TargetedDiscovery');
final targetedDiscoveryProvider = ViewProvider((ref) {
final httpTargetDiscoveryProvider = ViewProvider((ref) {
final dio = ref.watch(dioProvider).discovery;
final fingerprint = ref.watch(securityProvider).certificateHash;
return TargetedDiscoveryService(dio, fingerprint);
final fingerprint = ref.watch(syncProvider).securityContext.certificateHash;
return HttpTargetDiscoveryService(dio, fingerprint);
});
/// Try to discover a single device using the given IP and port.
class TargetedDiscoveryService {
class HttpTargetDiscoveryService {
final Dio _dio;
final String _fingerprint;
TargetedDiscoveryService(this._dio, this._fingerprint);
HttpTargetDiscoveryService(this._dio, this._fingerprint);
Future<Device?> discover({
required String ip,
@@ -0,0 +1,182 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:common/api_route_builder.dart';
import 'package:common/constants.dart';
import 'package:common/isolate.dart';
import 'package:common/model/device.dart';
import 'package:common/model/dto/multicast_dto.dart';
import 'package:common/model/dto/register_dto.dart';
import 'package:common/src/isolate/child/dio_provider.dart';
import 'package:common/util/sleep.dart';
import 'package:logging/logging.dart';
import 'package:refena/refena.dart';
final _logger = Logger('Multicast');
final multicastDiscoveryProvider = Provider((ref) {
return MulticastService(ref);
});
class MulticastService {
MulticastService(this._ref);
final Ref _ref;
bool _listening = false;
/// Binds the UDP port and listen to UDP multicast packages
/// It will automatically answer announcement messages
Stream<Device> startListener() async* {
if (_listening) {
_logger.info('Already listening to multicast');
return;
}
_listening = true;
final streamController = StreamController<Device>();
final syncState = _ref.read(syncProvider);
final sockets = await _getSockets(syncState.multicastGroup, syncState.port);
for (final socket in sockets) {
socket.socket.listen((_) {
final datagram = socket.socket.receive();
if (datagram == null) {
return;
}
try {
final dto = MulticastDto.fromJson(jsonDecode(utf8.decode(datagram.data)));
if (dto.fingerprint == syncState.securityContext.certificateHash) {
return;
}
final ip = datagram.address.address;
final peer = dto.toDevice(ip, syncState.port, syncState.protocol == ProtocolType.https);
streamController.add(peer);
if ((dto.announcement == true || dto.announce == true) && syncState.serverRunning) {
// only respond when server is running
_answerAnnouncement(peer);
}
} catch (e) {
_logger.warning('Could not parse multicast message', e);
}
});
_logger.info(
'Bind UDP multicast port (ip: ${socket.interface.addresses.map((a) => a.address).toList()}, group: ${syncState.multicastGroup}, port: ${syncState.port})',
);
}
// Tell everyone in the network that I am online
unawaited(
sendAnnouncement(),
);
yield* streamController.stream;
}
/// Sends an announcement which triggers a response on every LocalSend member of the network.
Future<void> sendAnnouncement() async {
final syncState = _ref.read(syncProvider);
final sockets = await _getSockets(syncState.multicastGroup);
final dto = _getMulticastDto(announcement: true);
for (final wait in [100, 500, 2000]) {
await sleepAsync(wait);
_logger.info('Announce via UDP');
for (final socket in sockets) {
try {
socket.socket.send(dto, InternetAddress(syncState.multicastGroup), syncState.port);
socket.socket.close();
} catch (e) {
_logger.warning('Could not send multicast message', e);
}
}
}
}
/// Responds to an announcement.
Future<void> _answerAnnouncement(Device peer) async {
try {
// Answer with TCP
await _ref.read(dioProvider).discovery.post(
ApiRoute.register.target(peer),
data: _getRegisterDto().toJson(),
);
_logger.info('Respond to announcement of ${peer.alias} (${peer.ip}, model: ${peer.deviceModel}) via TCP');
} catch (e) {
// Fallback: Answer with UDP
final syncState = _ref.read(syncProvider);
final sockets = await _getSockets(syncState.multicastGroup);
final dto = _getMulticastDto(announcement: false);
for (final socket in sockets) {
try {
socket.socket.send(dto, InternetAddress(syncState.multicastGroup), syncState.port);
socket.socket.close();
} catch (e) {
_logger.warning('Could not send multicast message', e);
}
}
_logger.info('Respond to announcement of ${peer.alias} (${peer.ip}, model: ${peer.deviceModel}) with UDP because TCP failed');
}
}
/// Returns the MulticastDto of this device in bytes.
List<int> _getMulticastDto({required bool announcement}) {
final syncState = _ref.read(syncProvider);
final dto = MulticastDto(
alias: syncState.alias,
version: protocolVersion,
deviceModel: syncState.deviceInfo.deviceModel,
deviceType: syncState.deviceInfo.deviceType,
fingerprint: syncState.securityContext.certificateHash,
port: syncState.port,
protocol: syncState.protocol,
download: syncState.download,
announcement: announcement,
announce: announcement,
);
return utf8.encode(jsonEncode(dto.toJson()));
}
RegisterDto _getRegisterDto() {
final syncState = _ref.read(syncProvider);
return RegisterDto(
alias: syncState.alias,
version: protocolVersion,
deviceModel: syncState.deviceInfo.deviceModel,
deviceType: syncState.deviceInfo.deviceType,
fingerprint: syncState.securityContext.certificateHash,
port: syncState.port,
protocol: syncState.protocol,
download: syncState.download,
);
}
}
class _SocketResult {
final NetworkInterface interface;
final RawDatagramSocket socket;
_SocketResult(this.interface, this.socket);
}
Future<List<_SocketResult>> _getSockets(String multicastGroup, [int? port]) async {
final interfaces = await NetworkInterface.list();
final sockets = <_SocketResult>[];
for (final interface in interfaces) {
try {
final socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, port ?? 0);
socket.joinMulticast(InternetAddress(multicastGroup), interface);
sockets.add(_SocketResult(interface, socket));
} catch (e) {
_logger.warning(
'Could not bind UDP multicast port (ip: ${interface.addresses.map((a) => a.address).toList()}, group: $multicastGroup, port: $port)',
e,
);
}
}
return sockets;
}
@@ -0,0 +1,24 @@
import 'package:common/src/isolate/child/sync_provider.dart';
import 'package:common/util/dio.dart';
import 'package:dio/dio.dart';
import 'package:refena/refena.dart';
class DioCollection {
final Dio discovery;
final Dio longLiving;
DioCollection({
required this.discovery,
required this.longLiving,
});
}
/// Provides a dio having a specific timeout.
/// Copied from app/lib/provider/dio_provider.dart
final dioProvider = ViewProvider((ref) {
final (securityContext, discoveryTimeout) = ref.watch(syncProvider.select((state) => (state.securityContext, state.discoveryTimeout)));
return DioCollection(
discovery: createDio(Duration(milliseconds: discoveryTimeout), securityContext),
longLiving: createDio(const Duration(days: 30), securityContext),
);
});

Some files were not shown because too many files have changed in this diff Show More