From fc477bd5031db6b47a093b216adb7932aa76c4c9 Mon Sep 17 00:00:00 2001 From: Vishesh Handa Date: Sun, 27 Sep 2020 22:46:18 +0200 Subject: [PATCH] Experiment with advertising a ZeroConf Service --- lib/main_dns.dart | 9 +- lib/main_zeroconf.dart | 258 +++++++++++++++++++++++++++++++++++++++++ pubspec.lock | 16 ++- pubspec.yaml | 3 +- 4 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 lib/main_zeroconf.dart diff --git a/lib/main_dns.dart b/lib/main_dns.dart index c6c85db0..ed45a783 100644 --- a/lib/main_dns.dart +++ b/lib/main_dns.dart @@ -5,6 +5,7 @@ Future main() async { var name = '_dartobservatory._tcp.local'; name = '_gitjournal._tcp'; + name = '_bonsoirdemo._tcp'; final MDnsClient client = MDnsClient(); // Start the client with default options. await client.start(); @@ -39,5 +40,9 @@ Future main() async { // Use connectivity plugin to get ssid info when connected to the wifi network // https://stackoverflow.com/questions/55716751/flutter-ios-reading-wifi-name-using-the-connectivity-or-wifi-plugin/55732656#55732656 -// For Registering on Mobiles - -// https://pub.dev/packages/bonsoir +// Use the WorkManager in Android to do the syncing in the background +// -> This should be configurable if we want to be able to sync in the background + +// TODO: +// * I don't know if/how I can run an HTTP Server in the Background +// Is that still possible with current Android Versions? diff --git a/lib/main_zeroconf.dart b/lib/main_zeroconf.dart new file mode 100644 index 00000000..e93a060c --- /dev/null +++ b/lib/main_zeroconf.dart @@ -0,0 +1,258 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; + +import 'package:bonsoir/bonsoir.dart'; +import 'package:device_info/device_info.dart'; +import 'package:provider/provider.dart'; + +/// Plugin's main method. +void main() => runApp(BonsoirExampleMainWidget()); + +/// Allows to get the Bonsoir service corresponding to the current device. +class AppService { + /// The service type. + static const String type = '_bonsoirdemo._tcp'; + + /// The service port (in this example we're not doing anything on that port but you should). + static const int port = 4000; + + /// The cached service. + static BonsoirService _service; + + /// Returns (and create if needed) the app Bonsoir service. + static Future getService() async { + if (_service != null) { + return _service; + } + + String name; + if (Platform.isAndroid) { + name = (await DeviceInfoPlugin().androidInfo).model; + } else if (Platform.isIOS) { + name = (await DeviceInfoPlugin().iosInfo).localizedModel; + } else { + name = 'Flutter'; + } + name += ' Bonsoir Demo'; + + _service = BonsoirService(name: name, type: type, port: port); + return _service; + } +} + +/// Provider model that allows to handle Bonsoir broadcasts. +class BonsoirBroadcastModel extends ChangeNotifier { + /// The current Bonsoir broadcast object instance. + BonsoirBroadcast _bonsoirBroadcast; + + /// Whether Bonsoir is currently broadcasting the app's service. + bool _isBroadcasting = false; + + /// Returns wether Bonsoir is currently broadcasting the app's service. + bool get isBroadcasting => _isBroadcasting; + + /// Starts the Bonsoir broadcast. + Future start({bool notify = true}) async { + if (_bonsoirBroadcast == null || _bonsoirBroadcast.isStopped) { + _bonsoirBroadcast = + BonsoirBroadcast(service: await AppService.getService()); + await _bonsoirBroadcast.ready; + } + + await _bonsoirBroadcast.start(); + _isBroadcasting = true; + if (notify) { + notifyListeners(); + } + } + + /// Stops the Bonsoir broadcast. + void stop({bool notify = true}) { + _bonsoirBroadcast?.stop(); + _isBroadcasting = false; + if (notify) { + notifyListeners(); + } + } + + @override + void dispose() { + stop(notify: false); + super.dispose(); + } +} + +/// Provider model that allows to handle Bonsoir discoveries. +class BonsoirDiscoveryModel extends ChangeNotifier { + /// The current Bonsoir discovery object instance. + BonsoirDiscovery _bonsoirDiscovery; + + /// Contains all discovered (and resolved) services. + final List _resolvedServices = []; + + /// The subscription object. + StreamSubscription _subscription; + + /// Creates a new Bonsoir discovery model instance. + BonsoirDiscoveryModel() { + start(); + } + + /// Returns all discovered (and resolved) services. + List get discoveredServices => + List.of(_resolvedServices); + + /// Starts the Bonsoir discovery. + Future start() async { + if (_bonsoirDiscovery == null || _bonsoirDiscovery.isStopped) { + _bonsoirDiscovery = + BonsoirDiscovery(type: (await AppService.getService()).type); + await _bonsoirDiscovery.ready; + } + + await _bonsoirDiscovery.start(); + _subscription = _bonsoirDiscovery.eventStream.listen(_onEventOccurred); + } + + /// Stops the Bonsoir discovery. + void stop() { + _subscription?.cancel(); + _subscription = null; + _bonsoirDiscovery?.stop(); + } + + /// Triggered when a Bonsoir discovery event occurred. + void _onEventOccurred(BonsoirDiscoveryEvent event) { + if (event.service == null || !event.isServiceResolved) { + return; + } + + if (event.type == BonsoirDiscoveryEventType.DISCOVERY_SERVICE_RESOLVED) { + _resolvedServices.add(event.service); + notifyListeners(); + } else if (event.type == BonsoirDiscoveryEventType.DISCOVERY_SERVICE_LOST) { + _resolvedServices.remove(event.service); + notifyListeners(); + } + } + + @override + void dispose() { + stop(); + super.dispose(); + } +} + +/// Allows to switch the app broadcast state. +class BroadcastSwitch extends StatelessWidget { + @override + Widget build(BuildContext context) { + BonsoirBroadcastModel model = context.watch(); + return InkWell( + onTap: () => _onTap(model), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Broadcast'.toUpperCase()), + Switch( + value: model.isBroadcasting, + onChanged: (value) => _onTap(model), + activeColor: Colors.white, + activeTrackColor: Colors.white54, + ), + ], + ), + ); + } + + /// Triggered when the widget has been tapped on. + void _onTap(BonsoirBroadcastModel model) { + if (model.isBroadcasting) { + model.stop(); + } else { + model.start(); + } + } +} + +/// Allows to display all discovered services. +class ServiceList extends StatelessWidget { + @override + Widget build(BuildContext context) { + BonsoirDiscoveryModel model = context.watch(); + List discoveredServices = model.discoveredServices; + if (discoveredServices.isEmpty) { + return const Padding( + padding: EdgeInsets.all(20), + child: Center( + child: Text( + 'Found no service of type "${AppService.type}".', + style: TextStyle( + color: Colors.black54, + fontStyle: FontStyle.italic, + ), + ), + ), + ); + } + + return ListView.builder( + itemCount: discoveredServices.length, + itemBuilder: (context, index) => + _ServiceWidget(service: discoveredServices[index]), + ); + } +} + +/// Allows to display a discovered service. +class _ServiceWidget extends StatelessWidget { + /// The discovered service. + final ResolvedBonsoirService service; + + /// Creates a new service widget. + const _ServiceWidget({ + @required this.service, + }); + + @override + Widget build(BuildContext context) => ListTile( + title: Text(service.name), + subtitle: Text( + 'Type : ${service.type}, ip : ${service.ip}, port : ${service.port}'), + ); +} + +/// Allows to display the app title based on how many services have been discovered. +class TitleWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + int count = + context.watch().discoveredServices.length; + return Text(count == 0 ? 'Bonsoir app demo' : 'Found $count service(s)'); + } +} + +/// The main widget. +class BonsoirExampleMainWidget extends StatelessWidget { + @override + Widget build(BuildContext context) => MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (context) => BonsoirBroadcastModel()), + ChangeNotifierProvider( + create: (context) => BonsoirDiscoveryModel()), + ], + builder: (context, child) => MaterialApp( + home: Scaffold( + appBar: AppBar( + title: TitleWidget(), + actions: [BroadcastSwitch()], + centerTitle: false, + ), + body: ServiceList(), + ), + ), + ); +} diff --git a/pubspec.lock b/pubspec.lock index 5515e31b..eb2fd02c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -64,6 +64,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.5" + bonsoir: + dependency: "direct main" + description: + name: bonsoir + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2+2" boolean_selector: dependency: transitive description: @@ -588,6 +595,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4" node_interop: dependency: transitive description: @@ -713,7 +727,7 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "4.3.2+2" pub_cache: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c935871e..706aca6a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,7 +28,7 @@ dependencies: fimber: ^0.3.0 dynamic_theme: ^1.0.0 flutter_staggered_grid_view: ^0.3.0 - provider: ^3.2.0 + provider: ^4.3.2+2 git_bindings: ^0.0.15 dart_git: git: https://github.com/GitJournal/dart_git.git @@ -57,6 +57,7 @@ dependencies: flutter_plugin_android_lifecycle: ^1.0.8 # for fixing the build timeago: ^2.0.27 multicast_dns: ^0.2.2 + bonsoir: ^0.1.2+2 dev_dependencies: flutter_launcher_icons: "^0.7.2"