Hide QR code scanner on devices without cameras (#101)

This commit is contained in:
John Maguire 2022-11-22 16:50:11 -05:00 committed by GitHub
parent 6b1bbf7352
commit a5139c4335
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 114 additions and 28 deletions

View File

@ -7,6 +7,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.ServiceConnection import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.net.VpnService import android.net.VpnService
import android.os.* import android.os.*
import android.util.Log import android.util.Log
@ -62,6 +63,7 @@ class MainActivity: FlutterActivity() {
ui!!.setMethodCallHandler { call, result -> ui!!.setMethodCallHandler { call, result ->
when(call.method) { when(call.method) {
"android.registerActiveSite" -> registerActiveSite(result) "android.registerActiveSite" -> registerActiveSite(result)
"android.deviceHasCamera" -> deviceHasCamera(result)
"nebula.parseCerts" -> nebulaParseCerts(call, result) "nebula.parseCerts" -> nebulaParseCerts(call, result)
"nebula.generateKeyPair" -> nebulaGenerateKeyPair(result) "nebula.generateKeyPair" -> nebulaGenerateKeyPair(result)
@ -120,6 +122,10 @@ class MainActivity: FlutterActivity() {
result.success(null) 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) { private fun nebulaParseCerts(call: MethodCall, result: MethodChannel.Result) {
val certs = call.argument<String>("certs") val certs = call.argument<String>("certs")
if (certs == "") { if (certs == "") {

View File

@ -21,6 +21,7 @@ class HostInfoScreen extends StatefulWidget {
required this.pending, required this.pending,
this.onChanged, this.onChanged,
required this.site, required this.site,
required this.supportsQRScanning,
}) : super(key: key); }) : super(key: key);
final bool isLighthouse; final bool isLighthouse;
@ -29,6 +30,8 @@ class HostInfoScreen extends StatefulWidget {
final Function? onChanged; final Function? onChanged;
final Site site; final Site site;
final bool supportsQRScanning;
@override @override
_HostInfoScreenState createState() => _HostInfoScreenState(); _HostInfoScreenState createState() => _HostInfoScreenState();
} }
@ -72,7 +75,10 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
labelWidth: 150, labelWidth: 150,
content: Text(hostInfo.cert!.details.name), content: Text(hostInfo.cert!.details.name),
onPressed: () => Utils.openPage( onPressed: () => Utils.openPage(
context, (context) => CertificateDetailsScreen(certInfo: CertificateInfo(cert: hostInfo.cert!)))) context, (context) => CertificateDetailsScreen(
certInfo: CertificateInfo(cert: hostInfo.cert!),
supportsQRScanning: widget.supportsQRScanning,
)))
: Container(), : Container(),
]); ]);
} }

View File

@ -74,6 +74,8 @@ class _MainScreenState extends State<MainScreen> {
// A set of widgets to display in a column that represents an error blocking us from moving forward entirely // A set of widgets to display in a column that represents an error blocking us from moving forward entirely
List<Widget>? error; List<Widget>? error;
bool supportsQRScanning = false;
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService'); static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
RefreshController refreshController = RefreshController(); RefreshController refreshController = RefreshController();
ScrollController scrollController = ScrollController(); ScrollController scrollController = ScrollController();
@ -123,6 +125,16 @@ class _MainScreenState extends State<MainScreen> {
); );
} }
// 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( return SimplePage(
title: Text('Nebula'), title: Text('Nebula'),
scrollable: SimpleScrollable.vertical, scrollable: SimpleScrollable.vertical,
@ -133,12 +145,11 @@ class _MainScreenState extends State<MainScreen> {
onPressed: () => Utils.openPage(context, (context) { onPressed: () => Utils.openPage(context, (context) {
return SiteConfigScreen(onSave: (_) { return SiteConfigScreen(onSave: (_) {
_loadSites(); _loadSites();
}); }, supportsQRScanning: supportsQRScanning);
}), }),
), ),
refreshController: refreshController, refreshController: refreshController,
onRefresh: () { onRefresh: () {
print("onRefresh");
_loadSites(); _loadSites();
refreshController.refreshCompleted(); refreshController.refreshCompleted();
}, },
@ -198,7 +209,11 @@ class _MainScreenState extends State<MainScreen> {
site: site, site: site,
onPressed: () { onPressed: () {
Utils.openPage(context, (context) { Utils.openPage(context, (context) {
return SiteDetailScreen(site: site, onChanged: () => _loadSites()); return SiteDetailScreen(
site: site,
onChanged: () => _loadSites(),
supportsQRScanning: supportsQRScanning,
);
}); });
})); }));
}); });

View File

@ -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) //TODO: ios is now the problem with connecting screwing our ability to query the hostmap (its a race)
class SiteDetailScreen extends StatefulWidget { 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 Site site;
final Function? onChanged; final Function? onChanged;
final bool supportsQRScanning;
@override @override
_SiteDetailScreenState createState() => _SiteDetailScreenState(); _SiteDetailScreenState createState() => _SiteDetailScreenState();
@ -193,7 +199,9 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
setState(() { setState(() {
activeHosts = hosts; activeHosts = hosts;
}); });
})); },
supportsQRScanning: widget.supportsQRScanning,
));
}, },
label: Text("Active"), label: Text("Active"),
content: Container(alignment: Alignment.centerRight, child: active)), content: Container(alignment: Alignment.centerRight, child: active)),
@ -211,7 +219,9 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
setState(() { setState(() {
pendingHosts = hosts; pendingHosts = hosts;
}); });
})); },
supportsQRScanning: widget.supportsQRScanning,
));
}, },
label: Text("Pending"), label: Text("Pending"),
content: Container(alignment: Alignment.centerRight, child: pending)) content: Container(alignment: Alignment.centerRight, child: pending))
@ -231,7 +241,9 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
onSave: (site) async { onSave: (site) async {
changed = true; changed = true;
setState(() {}); setState(() {});
}); },
supportsQRScanning: widget.supportsQRScanning,
);
}); });
}, },
), ),

View File

@ -10,8 +10,14 @@ import 'package:mobile_nebula/services/utils.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
class SiteTunnelsScreen extends StatefulWidget { class SiteTunnelsScreen extends StatefulWidget {
const SiteTunnelsScreen( const SiteTunnelsScreen({
{Key? key, required this.site, required this.tunnels, required this.pending, required this.onChanged}) Key? key,
required this.site,
required this.tunnels,
required this.pending,
required this.onChanged,
required this.supportsQRScanning,
})
: super(key: key); : super(key: key);
final Site site; final Site site;
@ -19,6 +25,8 @@ class SiteTunnelsScreen extends StatefulWidget {
final bool pending; final bool pending;
final Function(List<HostInfo>)? onChanged; final Function(List<HostInfo>)? onChanged;
final bool supportsQRScanning;
@override @override
_SiteTunnelsScreenState createState() => _SiteTunnelsScreenState(); _SiteTunnelsScreenState createState() => _SiteTunnelsScreenState();
} }
@ -67,7 +75,10 @@ class _SiteTunnelsScreenState extends State<SiteTunnelsScreen> {
site: widget.site, site: widget.site,
onChanged: () { onChanged: () {
_listHostmap(); _listHostmap();
})), },
supportsQRScanning: widget.supportsQRScanning,
),
),
label: Row(children: <Widget>[Padding(child: icon, padding: EdgeInsets.only(right: 10)), Text(hostInfo.vpnIp)]), label: Row(children: <Widget>[Padding(child: icon, padding: EdgeInsets.only(right: 10)), Text(hostInfo.vpnIp)]),
labelWidth: ipWidth, labelWidth: ipWidth,
content: Container(alignment: Alignment.centerRight, child: Text(hostInfo.cert?.details.name ?? "")), content: Container(alignment: Alignment.centerRight, child: Text(hostInfo.cert?.details.name ?? "")),

View File

@ -30,6 +30,7 @@ class AddCertificateScreen extends StatefulWidget {
this.onReplace, this.onReplace,
required this.pubKey, required this.pubKey,
required this.privKey, required this.privKey,
required this.supportsQRScanning,
}) : super(key: key); }) : super(key: key);
// onSave will pop a new CertificateDetailsScreen. // onSave will pop a new CertificateDetailsScreen.
@ -42,6 +43,8 @@ class AddCertificateScreen extends StatefulWidget {
final String pubKey; final String pubKey;
final String privKey; final String privKey;
final bool supportsQRScanning;
@override @override
_AddCertificateScreenState createState() => _AddCertificateScreenState(); _AddCertificateScreenState createState() => _AddCertificateScreenState();
} }
@ -100,6 +103,16 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
} }
List<Widget> _buildLoadCert() { List<Widget> _buildLoadCert() {
Map<String, Widget> 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<Widget> items = [ List<Widget> items = [
Padding( Padding(
padding: EdgeInsets.fromLTRB(10, 25, 10, 0), padding: EdgeInsets.fromLTRB(10, 25, 10, 0),
@ -112,11 +125,7 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
}); });
} }
}, },
children: { children: children,
'paste': Text('Copy/Paste'),
'file': Text('File'),
'qr': Text('QR Code'),
},
)) ))
]; ];
@ -124,7 +133,7 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
items.addAll(_addPaste()); items.addAll(_addPaste());
} else if (inputType == 'file') { } else if (inputType == 'file') {
items.addAll(_addFile()); items.addAll(_addFile());
} else { } else if (inputType == 'qr') {
items.addAll(_addQr()); items.addAll(_addQr());
} }
@ -257,7 +266,9 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
onSave: () { onSave: () {
Navigator.pop(context); Navigator.pop(context);
widget.onSave!(CertificateResult(certInfo: tryCertInfo, key: keyController.text)); widget.onSave!(CertificateResult(certInfo: tryCertInfo, key: keyController.text));
}); },
supportsQRScanning: widget.supportsQRScanning,
);
}); });
} }
} }

View File

@ -21,11 +21,14 @@ class CAListScreen extends StatefulWidget {
Key? key, Key? key,
required this.cas, required this.cas,
this.onSave, this.onSave,
required this.supportsQRScanning,
}) : super(key: key); }) : super(key: key);
final List<CertificateInfo> cas; final List<CertificateInfo> cas;
final ValueChanged<List<CertificateInfo>>? onSave; final ValueChanged<List<CertificateInfo>>? onSave;
final bool supportsQRScanning;
@override @override
_CAListScreenState createState() => _CAListScreenState(); _CAListScreenState createState() => _CAListScreenState();
} }
@ -88,7 +91,9 @@ class _CAListScreenState extends State<CAListScreen> {
changed = true; changed = true;
cas.remove(key); cas.remove(key);
}); });
}); },
supportsQRScanning: widget.supportsQRScanning,
);
}); });
}, },
)); ));
@ -129,6 +134,16 @@ class _CAListScreenState extends State<CAListScreen> {
} }
List<Widget> _addCA() { List<Widget> _addCA() {
Map<String, Widget> 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<Widget> items = [ List<Widget> items = [
Padding( Padding(
padding: EdgeInsets.fromLTRB(10, 25, 10, 0), padding: EdgeInsets.fromLTRB(10, 25, 10, 0),
@ -141,11 +156,7 @@ class _CAListScreenState extends State<CAListScreen> {
}); });
} }
}, },
children: { children: children,
'paste': Text('Copy/Paste'),
'file': Text('File'),
'qr': Text('QR Code'),
},
)) ))
]; ];

View File

@ -18,6 +18,7 @@ class CertificateDetailsScreen extends StatefulWidget {
this.onReplace, this.onReplace,
this.pubKey, this.pubKey,
this.privKey, this.privKey,
required this.supportsQRScanning,
}) : super(key: key); }) : super(key: key);
final CertificateInfo certInfo; final CertificateInfo certInfo;
@ -35,6 +36,8 @@ class CertificateDetailsScreen extends StatefulWidget {
final String? pubKey; final String? pubKey;
final String? privKey; final String? privKey;
final bool supportsQRScanning;
@override @override
_CertificateDetailsScreenState createState() => _CertificateDetailsScreenState(); _CertificateDetailsScreenState createState() => _CertificateDetailsScreenState();
} }
@ -178,7 +181,9 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
duration: const Duration(milliseconds: 10), curve: Curves.linearToEaseOut); duration: const Duration(milliseconds: 10), curve: Curves.linearToEaseOut);
}, },
pubKey: widget.pubKey!, pubKey: widget.pubKey!,
privKey: widget.privKey!); privKey: widget.privKey!,
supportsQRScanning: widget.supportsQRScanning,
);
}); });
}))); })));
} }

View File

@ -27,6 +27,7 @@ class SiteConfigScreen extends StatefulWidget {
Key? key, Key? key,
this.site, this.site,
required this.onSave, required this.onSave,
required this.supportsQRScanning,
}) : super(key: key); }) : super(key: key);
final Site? site; final Site? site;
@ -34,6 +35,8 @@ class SiteConfigScreen extends StatefulWidget {
// This is called after the target OS has saved the configuration // This is called after the target OS has saved the configuration
final ValueChanged<Site> onSave; final ValueChanged<Site> onSave;
final bool supportsQRScanning;
@override @override
_SiteConfigScreenState createState() => _SiteConfigScreenState(); _SiteConfigScreenState createState() => _SiteConfigScreenState();
} }
@ -186,7 +189,9 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
site.certInfo = result.certInfo; site.certInfo = result.certInfo;
site.key = result.key; site.key = result.key;
}); });
}); },
supportsQRScanning: widget.supportsQRScanning,
);
} }
return AddCertificateScreen( return AddCertificateScreen(
@ -198,7 +203,9 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
site.certInfo = result.certInfo; site.certInfo = result.certInfo;
site.key = result.key; site.key = result.key;
}); });
}); },
supportsQRScanning: widget.supportsQRScanning,
);
}); });
}, },
), ),
@ -222,7 +229,9 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
changed = true; changed = true;
site.ca = ca; site.ca = ca;
}); });
}); },
supportsQRScanning: widget.supportsQRScanning,
);
}); });
}) })
], ],