mirror of
https://github.com/localsend/localsend.git
synced 2026-06-23 04:10:07 +00:00
Discover in different thread (#1555)
This commit is contained in:
@@ -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,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
@@ -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,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';
|
||||
|
||||
@@ -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,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,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';
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,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,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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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,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,
|
||||
|
||||
@@ -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?> {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import 'package:common/common.dart';
|
||||
import 'package:common/model/device.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension DeviceTypeExt on DeviceType {
|
||||
|
||||
@@ -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,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,4 +1,4 @@
|
||||
import 'package:common/common.dart';
|
||||
import 'package:common/model/file_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension FileTypeExt on FileType {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,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,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,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,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';
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
+3
-3
@@ -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';
|
||||
+2
-1
@@ -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';
|
||||
+2
-2
@@ -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';
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
+12
-10
@@ -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
Reference in New Issue
Block a user