From a5139c43350601803330fd13ca3b178d5d252393 Mon Sep 17 00:00:00 2001 From: John Maguire Date: Tue, 22 Nov 2022 16:50:11 -0500 Subject: [PATCH] Hide QR code scanner on devices without cameras (#101) --- .../net/defined/mobile_nebula/MainActivity.kt | 6 +++++ lib/screens/HostInfoScreen.dart | 8 +++++- lib/screens/MainScreen.dart | 21 +++++++++++++--- lib/screens/SiteDetailScreen.dart | 20 ++++++++++++--- lib/screens/SiteTunnelsScreen.dart | 17 ++++++++++--- .../siteConfig/AddCertificateScreen.dart | 25 +++++++++++++------ lib/screens/siteConfig/CAListScreen.dart | 23 ++++++++++++----- .../siteConfig/CertificateDetailsScreen.dart | 7 +++++- lib/screens/siteConfig/SiteConfigScreen.dart | 15 ++++++++--- 9 files changed, 114 insertions(+), 28 deletions(-) diff --git a/android/app/src/main/kotlin/net/defined/mobile_nebula/MainActivity.kt b/android/app/src/main/kotlin/net/defined/mobile_nebula/MainActivity.kt index 9cf201a..6cab650 100644 --- a/android/app/src/main/kotlin/net/defined/mobile_nebula/MainActivity.kt +++ b/android/app/src/main/kotlin/net/defined/mobile_nebula/MainActivity.kt @@ -7,6 +7,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.ServiceConnection +import android.content.pm.PackageManager import android.net.VpnService import android.os.* import android.util.Log @@ -62,6 +63,7 @@ class MainActivity: FlutterActivity() { ui!!.setMethodCallHandler { call, result -> when(call.method) { "android.registerActiveSite" -> registerActiveSite(result) + "android.deviceHasCamera" -> deviceHasCamera(result) "nebula.parseCerts" -> nebulaParseCerts(call, result) "nebula.generateKeyPair" -> nebulaGenerateKeyPair(result) @@ -120,6 +122,10 @@ class MainActivity: FlutterActivity() { result.success(null) } + private fun deviceHasCamera(result: MethodChannel.Result) { + result.success(context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) + } + private fun nebulaParseCerts(call: MethodCall, result: MethodChannel.Result) { val certs = call.argument("certs") if (certs == "") { diff --git a/lib/screens/HostInfoScreen.dart b/lib/screens/HostInfoScreen.dart index 1662307..c9ac64a 100644 --- a/lib/screens/HostInfoScreen.dart +++ b/lib/screens/HostInfoScreen.dart @@ -21,6 +21,7 @@ class HostInfoScreen extends StatefulWidget { required this.pending, this.onChanged, required this.site, + required this.supportsQRScanning, }) : super(key: key); final bool isLighthouse; @@ -29,6 +30,8 @@ class HostInfoScreen extends StatefulWidget { final Function? onChanged; final Site site; + final bool supportsQRScanning; + @override _HostInfoScreenState createState() => _HostInfoScreenState(); } @@ -72,7 +75,10 @@ class _HostInfoScreenState extends State { labelWidth: 150, content: Text(hostInfo.cert!.details.name), onPressed: () => Utils.openPage( - context, (context) => CertificateDetailsScreen(certInfo: CertificateInfo(cert: hostInfo.cert!)))) + context, (context) => CertificateDetailsScreen( + certInfo: CertificateInfo(cert: hostInfo.cert!), + supportsQRScanning: widget.supportsQRScanning, + ))) : Container(), ]); } diff --git a/lib/screens/MainScreen.dart b/lib/screens/MainScreen.dart index 1f13e7a..7712d9a 100644 --- a/lib/screens/MainScreen.dart +++ b/lib/screens/MainScreen.dart @@ -74,6 +74,8 @@ class _MainScreenState extends State { // A set of widgets to display in a column that represents an error blocking us from moving forward entirely List? error; + bool supportsQRScanning = false; + static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService'); RefreshController refreshController = RefreshController(); ScrollController scrollController = ScrollController(); @@ -123,6 +125,16 @@ class _MainScreenState extends State { ); } + // Determine whether the device supports QR scanning. For example, some + // Chromebooks do not have camera support. + if (Platform.isAndroid) { + platform.invokeMethod("android.deviceHasCamera").then( + (hasCamera) => setState(() => supportsQRScanning = hasCamera) + ); + } else { + supportsQRScanning = true; + } + return SimplePage( title: Text('Nebula'), scrollable: SimpleScrollable.vertical, @@ -133,12 +145,11 @@ class _MainScreenState extends State { onPressed: () => Utils.openPage(context, (context) { return SiteConfigScreen(onSave: (_) { _loadSites(); - }); + }, supportsQRScanning: supportsQRScanning); }), ), refreshController: refreshController, onRefresh: () { - print("onRefresh"); _loadSites(); refreshController.refreshCompleted(); }, @@ -198,7 +209,11 @@ class _MainScreenState extends State { site: site, onPressed: () { Utils.openPage(context, (context) { - return SiteDetailScreen(site: site, onChanged: () => _loadSites()); + return SiteDetailScreen( + site: site, + onChanged: () => _loadSites(), + supportsQRScanning: supportsQRScanning, + ); }); })); }); diff --git a/lib/screens/SiteDetailScreen.dart b/lib/screens/SiteDetailScreen.dart index 0580501..a69b4c1 100644 --- a/lib/screens/SiteDetailScreen.dart +++ b/lib/screens/SiteDetailScreen.dart @@ -21,10 +21,16 @@ import 'package:pull_to_refresh/pull_to_refresh.dart'; //TODO: ios is now the problem with connecting screwing our ability to query the hostmap (its a race) class SiteDetailScreen extends StatefulWidget { - const SiteDetailScreen({Key? key, required this.site, this.onChanged}) : super(key: key); + const SiteDetailScreen({ + Key? key, + required this.site, + this.onChanged, + required this.supportsQRScanning, + }) : super(key: key); final Site site; final Function? onChanged; + final bool supportsQRScanning; @override _SiteDetailScreenState createState() => _SiteDetailScreenState(); @@ -193,7 +199,9 @@ class _SiteDetailScreenState extends State { setState(() { activeHosts = hosts; }); - })); + }, + supportsQRScanning: widget.supportsQRScanning, + )); }, label: Text("Active"), content: Container(alignment: Alignment.centerRight, child: active)), @@ -211,7 +219,9 @@ class _SiteDetailScreenState extends State { setState(() { pendingHosts = hosts; }); - })); + }, + supportsQRScanning: widget.supportsQRScanning, + )); }, label: Text("Pending"), content: Container(alignment: Alignment.centerRight, child: pending)) @@ -231,7 +241,9 @@ class _SiteDetailScreenState extends State { onSave: (site) async { changed = true; setState(() {}); - }); + }, + supportsQRScanning: widget.supportsQRScanning, + ); }); }, ), diff --git a/lib/screens/SiteTunnelsScreen.dart b/lib/screens/SiteTunnelsScreen.dart index 06f2ed1..133f4c3 100644 --- a/lib/screens/SiteTunnelsScreen.dart +++ b/lib/screens/SiteTunnelsScreen.dart @@ -10,8 +10,14 @@ import 'package:mobile_nebula/services/utils.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; class SiteTunnelsScreen extends StatefulWidget { - const SiteTunnelsScreen( - {Key? key, required this.site, required this.tunnels, required this.pending, required this.onChanged}) + const SiteTunnelsScreen({ + Key? key, + required this.site, + required this.tunnels, + required this.pending, + required this.onChanged, + required this.supportsQRScanning, + }) : super(key: key); final Site site; @@ -19,6 +25,8 @@ class SiteTunnelsScreen extends StatefulWidget { final bool pending; final Function(List)? onChanged; + final bool supportsQRScanning; + @override _SiteTunnelsScreenState createState() => _SiteTunnelsScreenState(); } @@ -67,7 +75,10 @@ class _SiteTunnelsScreenState extends State { site: widget.site, onChanged: () { _listHostmap(); - })), + }, + supportsQRScanning: widget.supportsQRScanning, + ), + ), label: Row(children: [Padding(child: icon, padding: EdgeInsets.only(right: 10)), Text(hostInfo.vpnIp)]), labelWidth: ipWidth, content: Container(alignment: Alignment.centerRight, child: Text(hostInfo.cert?.details.name ?? "")), diff --git a/lib/screens/siteConfig/AddCertificateScreen.dart b/lib/screens/siteConfig/AddCertificateScreen.dart index 943cb4e..6653514 100644 --- a/lib/screens/siteConfig/AddCertificateScreen.dart +++ b/lib/screens/siteConfig/AddCertificateScreen.dart @@ -30,6 +30,7 @@ class AddCertificateScreen extends StatefulWidget { this.onReplace, required this.pubKey, required this.privKey, + required this.supportsQRScanning, }) : super(key: key); // onSave will pop a new CertificateDetailsScreen. @@ -42,6 +43,8 @@ class AddCertificateScreen extends StatefulWidget { final String pubKey; final String privKey; + final bool supportsQRScanning; + @override _AddCertificateScreenState createState() => _AddCertificateScreenState(); } @@ -100,6 +103,16 @@ class _AddCertificateScreenState extends State { } List _buildLoadCert() { + Map children = { + 'paste': Text('Copy/Paste'), + 'file': Text('File'), + }; + + // not all devices have a camera for QR codes + if (widget.supportsQRScanning) { + children['qr'] = Text('QR Code'); + } + List items = [ Padding( padding: EdgeInsets.fromLTRB(10, 25, 10, 0), @@ -112,11 +125,7 @@ class _AddCertificateScreenState extends State { }); } }, - children: { - 'paste': Text('Copy/Paste'), - 'file': Text('File'), - 'qr': Text('QR Code'), - }, + children: children, )) ]; @@ -124,7 +133,7 @@ class _AddCertificateScreenState extends State { items.addAll(_addPaste()); } else if (inputType == 'file') { items.addAll(_addFile()); - } else { + } else if (inputType == 'qr') { items.addAll(_addQr()); } @@ -257,7 +266,9 @@ class _AddCertificateScreenState extends State { onSave: () { Navigator.pop(context); widget.onSave!(CertificateResult(certInfo: tryCertInfo, key: keyController.text)); - }); + }, + supportsQRScanning: widget.supportsQRScanning, + ); }); } } diff --git a/lib/screens/siteConfig/CAListScreen.dart b/lib/screens/siteConfig/CAListScreen.dart index 22ff968..65bcf72 100644 --- a/lib/screens/siteConfig/CAListScreen.dart +++ b/lib/screens/siteConfig/CAListScreen.dart @@ -21,11 +21,14 @@ class CAListScreen extends StatefulWidget { Key? key, required this.cas, this.onSave, + required this.supportsQRScanning, }) : super(key: key); final List cas; final ValueChanged>? onSave; + final bool supportsQRScanning; + @override _CAListScreenState createState() => _CAListScreenState(); } @@ -88,7 +91,9 @@ class _CAListScreenState extends State { changed = true; cas.remove(key); }); - }); + }, + supportsQRScanning: widget.supportsQRScanning, + ); }); }, )); @@ -129,6 +134,16 @@ class _CAListScreenState extends State { } List _addCA() { + Map children = { + 'paste': Text('Copy/Paste'), + 'file': Text('File'), + }; + + // not all devices have a camera for QR codes + if (widget.supportsQRScanning) { + children['qr'] = Text('QR Code'); + } + List items = [ Padding( padding: EdgeInsets.fromLTRB(10, 25, 10, 0), @@ -141,11 +156,7 @@ class _CAListScreenState extends State { }); } }, - children: { - 'paste': Text('Copy/Paste'), - 'file': Text('File'), - 'qr': Text('QR Code'), - }, + children: children, )) ]; diff --git a/lib/screens/siteConfig/CertificateDetailsScreen.dart b/lib/screens/siteConfig/CertificateDetailsScreen.dart index 1d3402c..1ca0ea6 100644 --- a/lib/screens/siteConfig/CertificateDetailsScreen.dart +++ b/lib/screens/siteConfig/CertificateDetailsScreen.dart @@ -18,6 +18,7 @@ class CertificateDetailsScreen extends StatefulWidget { this.onReplace, this.pubKey, this.privKey, + required this.supportsQRScanning, }) : super(key: key); final CertificateInfo certInfo; @@ -35,6 +36,8 @@ class CertificateDetailsScreen extends StatefulWidget { final String? pubKey; final String? privKey; + final bool supportsQRScanning; + @override _CertificateDetailsScreenState createState() => _CertificateDetailsScreenState(); } @@ -178,7 +181,9 @@ class _CertificateDetailsScreenState extends State { duration: const Duration(milliseconds: 10), curve: Curves.linearToEaseOut); }, pubKey: widget.pubKey!, - privKey: widget.privKey!); + privKey: widget.privKey!, + supportsQRScanning: widget.supportsQRScanning, + ); }); }))); } diff --git a/lib/screens/siteConfig/SiteConfigScreen.dart b/lib/screens/siteConfig/SiteConfigScreen.dart index 463a57d..7344561 100644 --- a/lib/screens/siteConfig/SiteConfigScreen.dart +++ b/lib/screens/siteConfig/SiteConfigScreen.dart @@ -27,6 +27,7 @@ class SiteConfigScreen extends StatefulWidget { Key? key, this.site, required this.onSave, + required this.supportsQRScanning, }) : super(key: key); final Site? site; @@ -34,6 +35,8 @@ class SiteConfigScreen extends StatefulWidget { // This is called after the target OS has saved the configuration final ValueChanged onSave; + final bool supportsQRScanning; + @override _SiteConfigScreenState createState() => _SiteConfigScreenState(); } @@ -186,7 +189,9 @@ class _SiteConfigScreenState extends State { site.certInfo = result.certInfo; site.key = result.key; }); - }); + }, + supportsQRScanning: widget.supportsQRScanning, + ); } return AddCertificateScreen( @@ -198,7 +203,9 @@ class _SiteConfigScreenState extends State { site.certInfo = result.certInfo; site.key = result.key; }); - }); + }, + supportsQRScanning: widget.supportsQRScanning, + ); }); }, ), @@ -222,7 +229,9 @@ class _SiteConfigScreenState extends State { changed = true; site.ca = ca; }); - }); + }, + supportsQRScanning: widget.supportsQRScanning, + ); }); }) ],