refactor: send tab and logging

This commit is contained in:
Tien Do Nam
2023-04-21 01:48:33 +02:00
parent d8e78451b5
commit 7af0343019
9 changed files with 121 additions and 85 deletions

View File

@@ -3,7 +3,7 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:localsend_app/pages/debug/multicast_debug_page.dart';
import 'package:localsend_app/pages/debug/discovery_debug_page.dart';
import 'package:localsend_app/provider/app_arguments_provider.dart';
import 'package:localsend_app/provider/fingerprint_provider.dart';
import 'package:localsend_app/widget/copyable_text.dart';
@@ -48,9 +48,9 @@ class DebugPage extends ConsumerWidget {
children: [
ElevatedButton(
onPressed: () async {
await context.push(() => const MulticastDebugPage());
await context.push(() => const DiscoveryDebugPage());
},
child: const Text('Multicast'),
child: const Text('Discovery Debugging'),
),
],
),

View File

@@ -1,22 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:localsend_app/provider/multicast_logs_provider.dart';
import 'package:localsend_app/provider/discovery_logs_provider.dart';
import 'package:localsend_app/provider/network/nearby_devices_provider.dart';
import 'package:localsend_app/widget/copyable_text.dart';
import 'package:localsend_app/widget/responsive_list_view.dart';
final _dateFormat = DateFormat.Hms();
class MulticastDebugPage extends ConsumerWidget {
const MulticastDebugPage({Key? key}) : super(key: key);
class DiscoveryDebugPage extends ConsumerWidget {
const DiscoveryDebugPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final logs = ref.watch(multicastLogsProvider);
final logs = ref.watch(discoveryLogsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Multicast'),
title: const Text('Discovery Debugging'),
),
body: ResponsiveListView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
@@ -29,7 +29,7 @@ class MulticastDebugPage extends ConsumerWidget {
),
const SizedBox(width: 20),
ElevatedButton(
onPressed: () => ref.read(multicastLogsProvider.notifier).clear(),
onPressed: () => ref.read(discoveryLogsProvider.notifier).clear(),
child: const Text('Clear'),
),
],

View File

@@ -1,6 +1,5 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/model/device.dart';
@@ -20,12 +19,12 @@ import 'package:localsend_app/provider/selection/selected_sending_files_provider
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/util/file_size_helper.dart';
import 'package:localsend_app/util/native/file_picker.dart';
import 'package:localsend_app/util/native/ios_network_permission_helper.dart';
import 'package:localsend_app/util/native/platform_check.dart';
import 'package:localsend_app/widget/big_button.dart';
import 'package:localsend_app/widget/custom_icon_button.dart';
import 'package:localsend_app/widget/dialogs/add_file_dialog.dart';
import 'package:localsend_app/widget/dialogs/address_input_dialog.dart';
import 'package:localsend_app/widget/dialogs/ios_network_permission_dialog.dart';
import 'package:localsend_app/widget/dialogs/no_files_dialog.dart';
import 'package:localsend_app/widget/dialogs/send_mode_help_dialog.dart';
import 'package:localsend_app/widget/file_thumbnail.dart';
@@ -46,37 +45,31 @@ class SendTab extends ConsumerStatefulWidget {
}
class _SendTabState extends ConsumerState<SendTab> {
static const iosCall = MethodChannel('localsend.localsend_app/iosCall');
final options = FilePickerOption.getOptionsForPlatform();
@override
void initState() {
super.initState();
// Automatically scan the network when visiting the scan tab
WidgetsBinding.instance.addPostFrameCallback((_) async {
final devices = ref.read(nearbyDevicesProvider.select((state) => state.devices));
if (devices.isEmpty) {
await ref.read(scanProvider).startSmartScan().whenComplete(() async {
if (devices.isEmpty) {
// After the first complete scan, if devices aren't found on IOS a Network trigger is called
if(checkPlatform([TargetPlatform.iOS])) {
try {
final bool granted = await iosCall.invokeMethod('triggerLocalNetworkDialog');
if (!granted) {
print("Local Network Permission denied");
if(context.mounted) await context.pushBottomSheet(() => const IosLocalNetworkDialog());
}
} on PlatformException catch (e) {
print(e);
}
}
}
});
}
WidgetsBinding.instance.addPostFrameCallback((_) {
// Automatically scan the network when visiting the scan tab
_init();
});
}
void _init() async {
final devices = ref.read(nearbyDevicesProvider.select((state) => state.devices));
if (devices.isEmpty) {
await ref.read(scanProvider).startSmartScan(forceLegacy: false);
if (devices.isEmpty) {
// After the first complete scan, if devices aren't found on IOS a Network trigger is called
if(checkPlatform([TargetPlatform.iOS]) && mounted) {
checkIosNetworkPermission(context);
}
}
}
}
@override
Widget build(BuildContext context) {
final sendMode = ref.watch(settingsProvider.select((s) => s.sendMode));
@@ -200,7 +193,6 @@ class _SendTabState extends ConsumerState<SendTab> {
const SizedBox(width: 10),
_ScanButton(
ips: networkInfo.localIps,
onSelect: (ip) async => ref.read(scanProvider).startLegacySubnetScan(ip),
),
Tooltip(
message: t.dialogs.addressInput.title,
@@ -307,6 +299,8 @@ class _SendTabState extends ConsumerState<SendTab> {
}
}
/// A button that opens a popup menu to select [T].
/// This is used for the scan button and the send mode button.
class _CircularPopupButton<T> extends StatelessWidget {
final String tooltip;
final PopupMenuItemBuilder<T> itemBuilder;
@@ -344,26 +338,25 @@ class _CircularPopupButton<T> extends StatelessWidget {
}
}
/// The scan button that uses [_CircularPopupButton].
class _ScanButton extends ConsumerWidget {
final List<String> ips;
final void Function(String ip) onSelect;
const _ScanButton({
required this.ips,
required this.onSelect,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final scanningIps = ref.watch(nearbyDevicesProvider.select((s) => s.runningIps));
if (ips.length <= 1) {
if (ips.length <= ScanFacade.maxInterfaces) {
return RotatingWidget(
duration: const Duration(seconds: 2),
spinning: scanningIps.isNotEmpty,
reverse: true,
child: CustomIconButton(
onPressed: () => onSelect(ips.first),
onPressed: () async => ref.read(scanProvider).startSmartScan(forceLegacy: true),
child: const Icon(Icons.sync),
),
);
@@ -371,7 +364,7 @@ class _ScanButton extends ConsumerWidget {
return _CircularPopupButton(
tooltip: t.sendTab.scan,
onSelected: (ip) => onSelect(ip),
onSelected: (ip) async => ref.read(scanProvider).startLegacySubnetScan([ip]),
itemBuilder: (_) {
return [
...ips.map(

View File

@@ -1,13 +1,13 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:localsend_app/model/log_entry.dart';
/// Contains the multicast logs for debugging purposes.
final multicastLogsProvider = NotifierProvider<MulticastLogsNotifier, List<LogEntry>>(() {
return MulticastLogsNotifier();
/// Contains the discovery logs for debugging purposes.
final discoveryLogsProvider = NotifierProvider<DiscoveryLogsNotifier, List<LogEntry>>(() {
return DiscoveryLogsNotifier();
});
class MulticastLogsNotifier extends Notifier<List<LogEntry>> {
MulticastLogsNotifier();
class DiscoveryLogsNotifier extends Notifier<List<LogEntry>> {
DiscoveryLogsNotifier();
@override
List<LogEntry> build() {

View File

@@ -9,8 +9,8 @@ import 'package:localsend_app/model/dto/multicast_dto.dart';
import 'package:localsend_app/model/dto/register_dto.dart';
import 'package:localsend_app/provider/device_info_provider.dart';
import 'package:localsend_app/provider/dio_provider.dart';
import 'package:localsend_app/provider/discovery_logs_provider.dart';
import 'package:localsend_app/provider/fingerprint_provider.dart';
import 'package:localsend_app/provider/multicast_logs_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/api_route_builder.dart';
@@ -60,16 +60,15 @@ class MulticastService {
final ip = datagram.address.address;
final peer = dto.toDevice(ip, settings.port, settings.https);
streamController.add(peer);
_ref.read(multicastLogsProvider.notifier).addLog('Received UDP: ${dto.alias} ($ip)');
if ((dto.announcement == true || dto.announce == true) && _ref.read(serverProvider) != null) {
// only respond when server is running
_answerAnnouncement(peer);
}
} catch (e) {
_ref.read(multicastLogsProvider.notifier).addLog(e.toString());
_ref.read(discoveryLogsProvider.notifier).addLog(e.toString());
}
});
_ref.read(multicastLogsProvider.notifier).addLog('Bind UDP multicast port (ip: ${socket.interface.addresses.map((a) => a.address).toList()}, group: ${settings.multicastGroup}, port: ${settings.port})');
_ref.read(discoveryLogsProvider.notifier).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
@@ -88,13 +87,13 @@ class MulticastService {
for (final wait in [100, 500, 2000]) {
await sleepAsync(wait);
_ref.read(multicastLogsProvider.notifier).addLog('Sending announcement');
_ref.read(discoveryLogsProvider.notifier).addLog('[ANNOUNCE/UDP]');
for (final socket in sockets) {
try {
socket.socket.send(dto, InternetAddress(settings.multicastGroup), settings.port);
socket.socket.close();
} catch (e) {
_ref.read(multicastLogsProvider.notifier).addLog(e.toString());
_ref.read(discoveryLogsProvider.notifier).addLog(e.toString());
}
}
}
@@ -110,9 +109,10 @@ class MulticastService {
ApiRoute.register.target(peer),
data: _getRegisterDto().toJson(),
);
_ref.read(multicastLogsProvider.notifier).addLog('Responded to announcement of ${peer.alias} (${peer.ip}, model: ${peer.deviceModel}) via TCP successfully.');
_ref.read(discoveryLogsProvider.notifier).addLog('[RESPONSE/TCP] Announcement of ${peer.alias} (${peer.ip}, model: ${peer.deviceModel}) via TCP');
} catch (e) {
// Fallback: Answer with UDP
print('EEE: $e');
final sockets = await _getSockets(settings.multicastGroup);
final dto = _getMulticastDto(announcement: false);
for (final socket in sockets) {
@@ -120,10 +120,10 @@ class MulticastService {
socket.socket.send(dto, InternetAddress(settings.multicastGroup), settings.port);
socket.socket.close();
} catch (e) {
_ref.read(multicastLogsProvider.notifier).addLog(e.toString());
_ref.read(discoveryLogsProvider.notifier).addLog(e.toString());
}
}
_ref.read(multicastLogsProvider.notifier).addLog('Responded to announcement of ${peer.alias} (${peer.ip}, model: ${peer.deviceModel}) with UDP because TCP failed.');
_ref.read(discoveryLogsProvider.notifier).addLog('[RESPONSE/UDP] Announcement of ${peer.alias} (${peer.ip}, model: ${peer.deviceModel}) with UDP because TCP failed');
}
}

View File

@@ -7,11 +7,17 @@ import 'package:localsend_app/model/device.dart';
import 'package:localsend_app/model/dto/info_dto.dart';
import 'package:localsend_app/model/state/nearby_devices_state.dart';
import 'package:localsend_app/provider/dio_provider.dart';
import 'package:localsend_app/provider/discovery_logs_provider.dart';
import 'package:localsend_app/provider/fingerprint_provider.dart';
import 'package:localsend_app/provider/network/multicast_provider.dart';
import 'package:localsend_app/util/api_route_builder.dart';
import 'package:localsend_app/util/task_runner.dart';
/// This provider is responsible for:
/// - Scanning the network for other LocalSend instances
/// - Keeping track of all found devices (they are only stored in RAM)
///
/// Use [scanProvider] to have a high-level API to perform discovery operations.
final nearbyDevicesProvider = NotifierProvider<NearbyDevicesNotifier, NearbyDevicesState>(() {
return NearbyDevicesNotifier();
});
@@ -36,17 +42,23 @@ class NearbyDevicesNotifier extends Notifier<NearbyDevicesState> {
);
}
void startMulticastListener() {
_multicastService.startListener().listen(registerDevice);
/// Binds the UDP port and listens for incoming announcements.
/// This should run forever as long as the app is running.
void startMulticastListener() async {
await for (final device in _multicastService.startListener()) {
registerDevice(device);
ref.read(discoveryLogsProvider.notifier).addLog('[DISCOVER/UDP] ${device.alias} (${device.ip}, model: ${device.deviceModel})');
}
}
/// It does not really "scan".
/// It just sends an announcement which will cause a response on every other LocalSend member of the network.
/// The responses have to be listened to by calling [startMulticastListener] first.
void startMulticastScan() {
unawaited(_multicastService.sendAnnouncement());
_multicastService.sendAnnouncement(); // ignore: discarded_futures
}
/// Scans one particular subnet with traditional HTTP/TCP discovery.
/// This method awaits until the scan is finished.
Future<void> startScan({required int port, required String localIp, required bool https}) async {
if (state.runningIps.contains(localIp)) {
// already running for the same localIp
@@ -68,7 +80,7 @@ class NearbyDevicesNotifier extends Notifier<NearbyDevicesState> {
_runners[networkInterface] = TaskRunner<Device?>(
initialTasks: List.generate(
ipList.length,
(index) => () async => _doRequest(_dio, ipList[index], port, https, fingerprint),
(index) => () async => _doRequest(ipList[index], port, https, fingerprint),
),
concurrency: 50,
);
@@ -81,23 +93,24 @@ class NearbyDevicesNotifier extends Notifier<NearbyDevicesState> {
devices: {...state.devices}..update(device.ip, (_) => device, ifAbsent: () => device),
);
}
}
Future<Device?> _doRequest(Dio dio, String currentIp, int port, bool https, String fingerprint) async {
print('Requesting $currentIp');
// We use the legacy route to make it less breaking for older versions
final url = ApiRoute.info.targetRaw(currentIp, port, https, peerProtocolVersion);
Device? device;
try {
final response = await dio.get(url, queryParameters: {
'fingerprint': fingerprint,
});
final dto = InfoDto.fromJson(response.data);
device = dto.toDevice(currentIp, port, https);
} on DioError catch (_) {
device = null;
} catch (e) {
device = null;
Future<Device?> _doRequest(String currentIp, int port, bool https, String fingerprint) async {
print('Requesting $currentIp');
// We use the legacy route to make it less breaking for older versions
final url = ApiRoute.info.targetRaw(currentIp, port, https, peerProtocolVersion);
Device? device;
try {
final response = await _dio.get(url, queryParameters: {
'fingerprint': fingerprint,
});
final dto = InfoDto.fromJson(response.data);
device = dto.toDevice(currentIp, port, https);
ref.read(discoveryLogsProvider.notifier).addLog('[DISCOVER/TCP] ${device.alias} (${device.ip}, model: ${device.deviceModel})');
} on DioError catch (_) {
device = null;
} catch (e) {
device = null;
}
return device;
}
return device;
}

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:localsend_app/provider/network/nearby_devices_provider.dart';
import 'package:localsend_app/provider/network_info_provider.dart';
@@ -11,29 +10,36 @@ final scanProvider = Provider((ref) => ScanFacade(ref));
/// Provides a simple API to make discovery operations.
class ScanFacade {
/// Maximum number of interfaces to scan.
/// If there are more interfaces, the first ones will be used or the user needs to select one.
static const maxInterfaces = 3;
final Ref _ref;
const ScanFacade(this._ref);
/// Scans the network via multicast first,
/// if no devices has been found, try http-based discovery on the first subnet
Future<void> startSmartScan() async {
Future<void> startSmartScan({required bool forceLegacy}) async {
// Try performant Multicast/UDP method first
_ref.read(nearbyDevicesProvider.notifier).startMulticastScan();
await sleepAsync(1000);
if (!forceLegacy) {
await sleepAsync(1000);
}
// If no devices has been found, then switch to legacy discovery mode
// which is purely HTTP/TCP based.
if (_ref.read(nearbyDevicesProvider).devices.isEmpty) {
final localIp = _ref.read(networkStateProvider).localIps.firstOrNull;
if (localIp != null) {
await startLegacySubnetScan(localIp);
if (forceLegacy || _ref.read(nearbyDevicesProvider).devices.isEmpty) {
final networkInterfaces = _ref.read(networkStateProvider).localIps.take(maxInterfaces).toList();
if (networkInterfaces.isNotEmpty) {
await startLegacySubnetScan(networkInterfaces);
}
}
}
/// HTTP based discovery on a single subnet.
Future<void> startLegacySubnetScan(String localIp) async {
/// HTTP based discovery on a fixed set of subnets.
Future<void> startLegacySubnetScan(List<String> subnets) async {
final settings = _ref.read(settingsProvider);
final port = settings.port;
final https = settings.https;
@@ -41,6 +47,8 @@ class ScanFacade {
// send announcement in parallel
_ref.read(nearbyDevicesProvider.notifier).startMulticastScan();
await _ref.read(nearbyDevicesProvider.notifier).startScan(port: port, localIp: localIp, https: https);
await Future.wait<void>([
for (final subnet in subnets) _ref.read(nearbyDevicesProvider.notifier).startScan(port: port, localIp: subnet, https: https),
]);
}
}

View File

@@ -16,6 +16,7 @@ import 'package:localsend_app/model/state/server/receiving_file.dart';
import 'package:localsend_app/pages/progress_page.dart';
import 'package:localsend_app/pages/receive_page.dart';
import 'package:localsend_app/provider/device_info_provider.dart';
import 'package:localsend_app/provider/discovery_logs_provider.dart';
import 'package:localsend_app/provider/network/nearby_devices_provider.dart';
import 'package:localsend_app/provider/network/server/server_utils.dart';
import 'package:localsend_app/provider/progress_provider.dart';
@@ -147,6 +148,7 @@ class ReceiveController {
// Save device information
server.ref.read(nearbyDevicesProvider.notifier).registerDevice(requestDto.toDevice(request.ip, port, https));
server.ref.read(discoveryLogsProvider.notifier).addLog('[DISCOVER/TCP] Received "/register" HTTP request: ${requestDto.alias} (${request.ip})');
final deviceInfo = server.ref.read(deviceRawInfoProvider);

View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:localsend_app/widget/dialogs/ios_network_permission_dialog.dart';
import 'package:routerino/routerino.dart';
const iosCall = MethodChannel('localsend.localsend_app/iosCall');
/// Checks if local network permission is granted on iOS.
/// If not, a dialog will be shown.
void checkIosNetworkPermission(BuildContext context) async {
try {
final bool granted = await iosCall.invokeMethod('triggerLocalNetworkDialog');
if (!granted) {
print("Local Network Permission denied");
if(context.mounted) await context.pushBottomSheet(() => const IosLocalNetworkDialog());
}
} on PlatformException catch (e) {
print(e);
}
}