diff --git a/analysis_options.yaml b/analysis_options.yaml index b148779e..2395de07 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -26,9 +26,5 @@ linter: sort_constructors_first: true - prefer_single_quotes: true - # Good packages document everything public_member_api_docs: true - - lines_longer_than_80_chars: true diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index c6c432c0..65ff0849 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -3,5 +3,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml index c6c432c0..65ff0849 100644 --- a/android/app/src/profile/AndroidManifest.xml +++ b/android/app/src/profile/AndroidManifest.xml @@ -3,5 +3,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/injection.dart b/lib/injection.dart new file mode 100644 index 00000000..2795ab62 --- /dev/null +++ b/lib/injection.dart @@ -0,0 +1,23 @@ +import 'package:get_it/get_it.dart'; +import 'package:injectable/injectable.dart'; +import 'package:vernet/injection.config.dart'; + +final getIt = GetIt.instance; + +/// Saves the current environment for manual use +late String currentEnv; + +@injectableInit +void configureDependencies(String env) { + currentEnv = env; + $initGetIt(getIt, environment: env); +} + +abstract class Env { + static const String test = 'test'; + static const String dev = 'dev'; + static const String prod = 'prod'; + + /// Demo of the app with fake data + static const String demo = 'demo'; +} diff --git a/lib/main.dart b/lib/main.dart index cba14e19..00777287 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import 'package:vernet/api/update_checker.dart'; import 'package:vernet/helper/app_settings.dart'; import 'package:vernet/helper/consent_loader.dart'; +import 'package:vernet/injection.dart'; import 'package:vernet/models/dark_theme_provider.dart'; import 'package:vernet/pages/home_page.dart'; import 'package:vernet/pages/location_consent_page.dart'; @@ -10,6 +11,7 @@ import 'package:vernet/pages/settings_page.dart'; late AppSettings appSettings; Future main() async { + configureDependencies(Env.prod); WidgetsFlutterBinding.ensureInitialized(); final bool allowed = await ConsentLoader.isConsentPageShown(); diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index dbfb0f58..12ac8b00 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -9,7 +9,7 @@ import 'package:vernet/models/internet_provider.dart'; import 'package:vernet/models/wifi_info.dart'; import 'package:vernet/pages/dns/dns_page.dart'; import 'package:vernet/pages/dns/reverse_dns_page.dart'; -import 'package:vernet/pages/host_scan_page.dart'; +import 'package:vernet/pages/host_scan_page/host_scan_page.dart'; import 'package:vernet/pages/network_troubleshoot/ping_page.dart'; import 'package:vernet/pages/network_troubleshoot/port_scan_page.dart'; import 'package:vernet/ui/custom_tile.dart'; @@ -89,7 +89,7 @@ class _WifiDetailState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => const HostScanPage(), + builder: (context) => HostScanPage(), ), ); }, diff --git a/lib/pages/host_scan_page.dart b/lib/pages/host_scan_page.dart deleted file mode 100644 index 6c97c051..00000000 --- a/lib/pages/host_scan_page.dart +++ /dev/null @@ -1,189 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:network_info_plus/network_info_plus.dart'; -import 'package:network_tools/network_tools.dart'; -import 'package:percent_indicator/percent_indicator.dart'; -import 'package:vernet/main.dart'; -import 'package:vernet/pages/network_troubleshoot/port_scan_page.dart'; - -class HostScanPage extends StatefulWidget { - const HostScanPage({Key? key}) : super(key: key); - - @override - _HostScanPageState createState() => _HostScanPageState(); -} - -class _HostScanPageState extends State - with TickerProviderStateMixin { - final Set _hosts = {}; - double _progress = 0; - bool _isScanning = false; - StreamSubscription? _streamSubscription; - late String? _ip; - late String? _gatewayIP; - - Future _getDevices() async { - _hosts.clear(); - _ip = await NetworkInfo().getWifiIP(); - _gatewayIP = await NetworkInfo().getWifiGatewayIP(); - - if (_ip != null && _ip!.isNotEmpty) { - final String subnet = _ip!.substring(0, _ip!.lastIndexOf('.')); - setState(() { - _isScanning = true; - }); - - final stream = HostScanner.discover( - subnet, - firstSubnet: appSettings.firstSubnet, - lastSubnet: appSettings.lastSubnet, - progressCallback: (progress) { - if (mounted) { - setState(() { - _progress = progress; - }); - } - }, - ); - - _streamSubscription = stream.listen( - (ActiveHost host) { - setState(() { - _hosts.add(host); - }); - }, - onDone: () { - if (mounted) { - setState(() { - _isScanning = false; - }); - } - }, - onError: (error) { - if (mounted) { - setState(() { - _isScanning = false; - }); - } - }, - ); - } - } - - @override - void initState() { - super.initState(); - _getDevices(); - } - - @override - void dispose() { - super.dispose(); - _streamSubscription?.cancel(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Scan for Devices'), - actions: [ - if (_isScanning) - Container( - margin: const EdgeInsets.only(right: 20.0), - child: CircularPercentIndicator( - radius: 20.0, - lineWidth: 2.5, - percent: _progress / 100, - backgroundColor: Colors.grey, - progressColor: Colors.white, - ), - ) - else - IconButton( - onPressed: _getDevices, - icon: const Icon(Icons.refresh), - ), - ], - ), - body: Center( - child: buildListView(context), - ), - ); - } - - Widget buildListView(BuildContext context) { - if (_progress >= 100 && _hosts.isEmpty) { - return const Text( - 'No host found.\nTry changing first and last subnet in settings', - textAlign: TextAlign.center, - ); - } else if (_isScanning && _hosts.isEmpty) { - return const CircularProgressIndicator.adaptive(); - } - - return Column( - children: [ - Expanded( - child: ListView.builder( - itemCount: _hosts.length, - itemBuilder: (context, index) { - final ActiveHost host = - SplayTreeSet.from(_hosts).toList()[index] as ActiveHost; - return ListTile( - leading: _getHostIcon(host.ip), - title: Text(_getDeviceMake(host)), - subtitle: Text(host.ip), - trailing: IconButton( - tooltip: 'Scan open ports for this target', - icon: const Icon(Icons.radar), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PortScanPage(target: host.ip), - ), - ); - }, - ), - onLongPress: () { - Clipboard.setData(ClipboardData(text: host.ip)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('IP copied to clipboard'), - ), - ); - }, - ); - }, - ), - ) - ], - ); - } - - String _getDeviceMake(ActiveHost host) { - if (_ip == host.ip) { - return 'This device'; - } else if (_gatewayIP == host.ip) { - return 'Router/Gateway'; - } - return host.make; - } - - Icon _getHostIcon(String hostIp) { - if (hostIp == _ip) { - if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { - return Icon(Icons.computer); - } - return Icon(Icons.smartphone); - } else if (hostIp == _gatewayIP) { - return Icon(Icons.router); - } - return Icon(Icons.devices); - } -} diff --git a/lib/pages/host_scan_page/device_in_the_network.dart b/lib/pages/host_scan_page/device_in_the_network.dart new file mode 100644 index 00000000..bb20aa10 --- /dev/null +++ b/lib/pages/host_scan_page/device_in_the_network.dart @@ -0,0 +1,102 @@ +import 'dart:io'; + +import 'package:dart_ping/dart_ping.dart'; +import 'package:flutter/material.dart'; +import 'package:network_tools/network_tools.dart'; + +/// Contains all the information of a device in the network including +/// icon, open ports and in the future host name and mDNS name +class DeviceInTheNetwork { + /// Create basic device with default (not the correct) icon + DeviceInTheNetwork({ + required this.ip, + required this.make, + required this.pingData, + this.iconData = Icons.devices, + this.hostId, + }); + + /// Create the object from active host with the correct field and icon + factory DeviceInTheNetwork.createFromActiveHost({ + required ActiveHost activeHost, + required String currentDeviceIp, + required String gatewayIp, + }) { + return DeviceInTheNetwork.createWithAllNecessaryFields( + ip: activeHost.ip, + hostId: activeHost.hostId, + make: activeHost.make, + pingData: activeHost.pingData, + currentDeviceIp: currentDeviceIp, + gatewayIp: gatewayIp, + ); + } + + /// Create the object with the correct field and icon + factory DeviceInTheNetwork.createWithAllNecessaryFields({ + required String ip, + required int hostId, + required String make, + required PingData pingData, + required String currentDeviceIp, + required String gatewayIp, + }) { + final IconData iconData = getHostIcon( + currentDeviceIp: currentDeviceIp, + hostIp: ip, + gatewayIp: gatewayIp, + ); + + final String deviceMake = getDeviceMake( + currentDeviceIp: currentDeviceIp, + hostIp: ip, + gatewayIp: gatewayIp, + hostMake: make, + ); + + return DeviceInTheNetwork( + ip: ip, + make: deviceMake, + pingData: pingData, + hostId: hostId, + iconData: iconData, + ); + } + + /// Ip of the device + final String ip; + final String make; + final PingData pingData; + final IconData iconData; + int? hostId; + + static String getDeviceMake({ + required String currentDeviceIp, + required String hostIp, + required String gatewayIp, + required String hostMake, + }) { + if (currentDeviceIp == hostIp) { + return 'This device'; + } else if (gatewayIp == hostIp) { + return 'Router/Gateway'; + } + return hostMake; + } + + static IconData getHostIcon({ + required String currentDeviceIp, + required String hostIp, + required String gatewayIp, + }) { + if (hostIp == currentDeviceIp) { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return Icons.computer; + } + return Icons.smartphone; + } else if (hostIp == gatewayIp) { + return Icons.router; + } + return Icons.devices; + } +} diff --git a/lib/pages/host_scan_page/host_scan_page.dart b/lib/pages/host_scan_page/host_scan_page.dart new file mode 100644 index 00000000..d281e103 --- /dev/null +++ b/lib/pages/host_scan_page/host_scan_page.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:vernet/injection.dart'; +import 'package:vernet/pages/host_scan_page/host_scna_bloc/host_scan_bloc.dart'; +import 'package:vernet/pages/host_scan_page/widgets/host_scan_widget.dart'; + +class HostScanPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Scan for Devices'), + // actions: [ + // if (_isScanning) + // Container( + // margin: const EdgeInsets.only(right: 20.0), + // child: CircularPercentIndicator( + // radius: 10.0, + // lineWidth: 2.5, + // percent: _progress / 100, + // backgroundColor: Colors.grey, + // progressColor: Colors.white, + // ), + // ) + // else + // IconButton( + // onPressed: _getDevices, + // icon: const Icon(Icons.refresh), + // ), + // ], + ), + body: Center( + child: BlocProvider( + create: (context) => + getIt()..add(const HostScanEvent.initialized()), + child: HostScanWidget(), + ), + ), + ); + } +} diff --git a/lib/pages/host_scan_page/host_scna_bloc/host_scan_bloc.dart b/lib/pages/host_scan_page/host_scna_bloc/host_scan_bloc.dart new file mode 100644 index 00000000..6414c722 --- /dev/null +++ b/lib/pages/host_scan_page/host_scna_bloc/host_scan_bloc.dart @@ -0,0 +1,129 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; +import 'package:isolate_contactor/isolate_contactor.dart'; +import 'package:network_info_plus/network_info_plus.dart'; +import 'package:network_tools/network_tools.dart'; +import 'package:vernet/main.dart'; +import 'package:vernet/pages/host_scan_page/device_in_the_network.dart'; + +part 'host_scan_bloc.freezed.dart'; +part 'host_scan_event.dart'; +part 'host_scan_state.dart'; + +@injectable +class HostScanBloc extends Bloc { + HostScanBloc() : super(HostScanState.initial()) { + on(_initialized); + on(_startNewScan); + } + + /// IP of the device in the local network. + String? ip; + + /// Gateway IP of the current network + late String? gatewayIp; + + String? subnet; + + /// List of all ActiveHost devices that got found in the current scan + List activeHostList = []; + + Future _initialized( + Initialized event, + Emitter emit, + ) async { + emit(const HostScanState.loadInProgress()); + ip = await NetworkInfo().getWifiIP(); + subnet = ip!.substring(0, ip!.lastIndexOf('.')); + gatewayIp = await NetworkInfo().getWifiGatewayIP(); + + add(const HostScanEvent.startNewScan()); + } + + Future _startNewScan( + StartNewScan event, + Emitter emit, + ) async { + const int scanRangeForIsolate = 51; + for (int i = appSettings.firstSubnet; + i <= appSettings.lastSubnet; + i += scanRangeForIsolate + 1) { + final IsolateContactor isolateContactor = + await IsolateContactor.createOwnIsolate(startSearchingDevices); + int limit = i + scanRangeForIsolate; + if (limit >= appSettings.lastSubnet) { + limit = appSettings.lastSubnet; + } + isolateContactor.sendMessage([ + subnet!, + i.toString(), + limit.toString(), + ]); + await for (final dynamic message in isolateContactor.onMessage) { + try { + if (message is ActiveHost) { + final DeviceInTheNetwork tempDeviceInTheNetwork = + DeviceInTheNetwork.createFromActiveHost( + activeHost: message, + currentDeviceIp: ip!, + gatewayIp: gatewayIp!, + ); + + activeHostList.add(tempDeviceInTheNetwork); + activeHostList.sort((a, b) { + final int aIp = + int.parse(a.ip.substring(a.ip.lastIndexOf('.') + 1)); + final int bIp = + int.parse(b.ip.substring(b.ip.lastIndexOf('.') + 1)); + return aIp.compareTo(bIp); + }); + emit(const HostScanState.loadInProgress()); + emit(HostScanState.foundNewDevice(activeHostList)); + } else if (message is String && message == 'Done') { + isolateContactor.dispose(); + } + } catch (e) { + emit(const HostScanState.error()); + } + } + } + print('The end of the scan'); + + // emit(HostScanState.loadSuccess(activeHostList)); + } + + /// Will search devices in the network inside new isolate + static Future startSearchingDevices(dynamic params) async { + final channel = IsolateContactorController(params); + channel.onIsolateMessage.listen((message) async { + List paramsListString = []; + if (message is List) { + paramsListString = message; + } else { + return; + } + + final String subnetIsolate = paramsListString[0]; + final int firstSubnetIsolate = int.parse(paramsListString[1]); + final int lastSubnetIsolate = int.parse(paramsListString[2]); + print('scanning from $firstSubnetIsolate to $lastSubnetIsolate'); + + /// Will contain all the hosts that got discovered in the network, will + /// be use inorder to cancel on dispose of the page. + final Stream hostsDiscoveredInNetwork = HostScanner.discover( + subnetIsolate, + firstSubnet: firstSubnetIsolate, + lastSubnet: lastSubnetIsolate, + ); + + await for (final ActiveHost activeHostFound in hostsDiscoveredInNetwork) { + channel.sendResult(activeHostFound); + } + channel.sendResult('Done'); + }); + } +} diff --git a/lib/pages/host_scan_page/host_scna_bloc/host_scan_event.dart b/lib/pages/host_scan_page/host_scna_bloc/host_scan_event.dart new file mode 100644 index 00000000..88e500f7 --- /dev/null +++ b/lib/pages/host_scan_page/host_scna_bloc/host_scan_event.dart @@ -0,0 +1,8 @@ +part of 'host_scan_bloc.dart'; + +@freezed +class HostScanEvent with _$HostScanEvent { + const factory HostScanEvent.initialized() = Initialized; + + const factory HostScanEvent.startNewScan() = StartNewScan; +} diff --git a/lib/pages/host_scan_page/host_scna_bloc/host_scan_state.dart b/lib/pages/host_scan_page/host_scna_bloc/host_scan_state.dart new file mode 100644 index 00000000..4b35f502 --- /dev/null +++ b/lib/pages/host_scan_page/host_scna_bloc/host_scan_state.dart @@ -0,0 +1,20 @@ +part of 'host_scan_bloc.dart'; + +@freezed +class HostScanState with _$HostScanState { + factory HostScanState.initial() = _Initial; + + const factory HostScanState.loadInProgress() = _LoadInProgress; + + const factory HostScanState.foundNewDevice( + List activeHostList, + ) = FoundNewDevice; + + const factory HostScanState.loadSuccess( + List activeHostList, + ) = LoadSuccess; + + const factory HostScanState.loadFailure() = _loadFailure; + + const factory HostScanState.error() = Error; +} diff --git a/lib/pages/host_scan_page/widgets/host_scan_widget.dart b/lib/pages/host_scan_page/widgets/host_scan_widget.dart new file mode 100644 index 00000000..ec2fc6bd --- /dev/null +++ b/lib/pages/host_scan_page/widgets/host_scan_widget.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:vernet/pages/host_scan_page/device_in_the_network.dart'; +import 'package:vernet/pages/host_scan_page/host_scna_bloc/host_scan_bloc.dart'; +import 'package:vernet/pages/network_troubleshoot/port_scan_page.dart'; + +class HostScanWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Column( + children: [ + BlocBuilder( + builder: (context, state) { + return state.map( + initial: (_) => Container(), + loadInProgress: (value) { + return Expanded( + child: Container( + margin: const EdgeInsets.all(30), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + CircularProgressIndicator(), + SizedBox( + height: 30, + ), + Text( + 'Searching for devices in your local network', + style: TextStyle( + fontSize: 18, + color: Colors.blue, + ), + ), + ], + ), + ), + ); + }, + foundNewDevice: (FoundNewDevice value) { + final List activeHostList = + value.activeHostList; + + return Expanded( + child: ListView.builder( + itemCount: activeHostList.length, + itemBuilder: (context, index) { + final DeviceInTheNetwork host = activeHostList[index]; + return ListTile( + leading: Icon(host.iconData), + title: Text(host.make), + subtitle: Text(host.ip), + trailing: IconButton( + tooltip: 'Scan open ports for this target', + icon: const Icon(Icons.radar), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + PortScanPage(target: host.ip), + ), + ); + }, + ), + onLongPress: () { + Clipboard.setData(ClipboardData(text: host.ip)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('IP copied to clipboard'), + ), + ); + }, + ); + }, + ), + ); + }, + loadFailure: (value) { + return const Text('Failure'); + }, + loadSuccess: (value) { + return const Text('Done'); + }, + error: (Error value) { + return const Text('Error'); + }, + ); + }, + ), + ], + ); + } +} diff --git a/lib/pages/network_troubleshoot/port_scan_page.dart b/lib/pages/network_troubleshoot/port_scan_page.dart index f6e730d6..b763392d 100644 --- a/lib/pages/network_troubleshoot/port_scan_page.dart +++ b/lib/pages/network_troubleshoot/port_scan_page.dart @@ -226,7 +226,7 @@ class _PortScanPageState extends State Container( margin: const EdgeInsets.only(right: 20.0), child: CircularPercentIndicator( - radius: 20.0, + radius: 10.0, lineWidth: 2.5, percent: _progress / 100, backgroundColor: Colors.grey, @@ -464,7 +464,7 @@ class _PortScanPageState extends State ], ), ), - Divider(height: 4), + const Divider(height: 4), ], ); }, diff --git a/pubspec.yaml b/pubspec.yaml index f03fec06..8ce2d829 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,27 +21,57 @@ environment: sdk: ">=2.12.0 <3.0.0" dependencies: + # Automatically resizes text to fit perfectly within its bounds. + auto_size_text: ^3.0.0 + # Helps implement the BLoC pattern. + bloc: ^8.0.2 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.4 dart_ping: ^6.1.1 flutter: sdk: flutter + # Bloc for state management, replace StatefulWidget + flutter_bloc: ^8.0.1 + # Annotations for freezed + freezed_annotation: ^1.1.0 + # Service locator + get_it: ^7.2.0 + # A composable, multi-platform, Future-based API for HTTP requests. http: ^0.13.4 + # Convenient code generator for get_it + injectable: ^1.5.3 + # An easy way to create a new isolate, keep it running and communicate with it. + isolate_contactor: ^1.2.0+1 + # Discover network info and configure themselves accordingly network_info_plus: ^2.1.2 - network_tools: ^1.0.7 + # Helps you discover open ports, devices on subnet and more. + network_tools: ^1.0.8 + # Querying information about the application package, such as CFBundleVersion package_info_plus: ^1.3.0 - percent_indicator: ^3.4.0 + # Allows you to display progress widgets based on percentage. + percent_indicator: ^4.0.0 + # Popup that ask for the requested permission permission_handler: ^8.3.0 + # A wrapper around InheritedWidget to make them easier to use and more reusable. provider: ^6.0.2 - shared_preferences: ^2.0.12 - url_launcher: ^6.0.18 + # Reading and writing simple key-value pairs + shared_preferences: ^2.0.13 + # Plugin for launching a URL + url_launcher: ^6.0.20 dev_dependencies: + # A build system for Dart code generation and modular compilation. + build_runner: + flutter_test: sdk: flutter + # Code generator for unions/pattern-matching/copy. + freezed: ^1.1.1 + # Convenient code generator for get_it. + injectable_generator: ^1.5.3 # Collection of lint rules for Dart and Flutter projects. - lint: ^1.8.1 + lint: ^1.8.2 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec