diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index c41dbd92..bb74edfe 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -50,7 +50,7 @@
NSLocationAlwaysAndWhenInUseUsageDescription
Application needs Your location to show nearby aircraft.
NSLocationAlwaysUsageDescription
- Application needs Your location to show nearby aircraft.
+ Application needs background location for Drone Radar feature.
NSLocationWhenInUseUsageDescription
Application needs Your location to show nearby aircraft.
UIApplicationSupportsIndirectInputEvents
diff --git a/lib/bloc/standards_cubit.dart b/lib/bloc/standards_cubit.dart
index c566687e..49fb20b4 100644
--- a/lib/bloc/standards_cubit.dart
+++ b/lib/bloc/standards_cubit.dart
@@ -8,19 +8,25 @@ import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart';
class StandardsState {
- bool androidSystem = false;
- bool btLegacy = false; // bt4
- bool btExtended = false; // bt4
- bool wifiBeacon = false;
- bool wifiNaN = false; // aware on android
- bool btExtendedClaimed = false; // can be claimed but still not work
+ final bool androidSystem;
+ final bool btLegacy; // bt4
+ final bool btExtended; // bt5
+ final bool wifiBeacon;
+ final bool wifiNaN; // aware on android
+ final bool btExtendedClaimed; // can be claimed but still not work
// if adv len is < 1000, we stronly suspect Bt long range scanning wont work
- int maxAdvDataLen = 0;
+ final int maxAdvDataLen;
// permissions
- bool btEnabled = false;
- bool locationEnabled = false;
- bool notificationsEnabled = false;
- bool internetAvailable = false;
+ final bool btEnabled;
+ final bool locationEnabled;
+ final bool backgroundLocationEnabled;
+ // true if user was asked about background location and cancelled request
+ final bool backgroundLocationDenied;
+ final bool notificationsEnabled;
+ final bool internetAvailable;
+
+ static const logPlatformStandardsKey = 'logPlatformStandards';
+ static const backgroundLocationDeniedKey = 'backgroundLocationDenied';
StandardsState({
required this.androidSystem,
@@ -32,6 +38,8 @@ class StandardsState {
required this.maxAdvDataLen,
required this.btEnabled,
required this.locationEnabled,
+ required this.backgroundLocationEnabled,
+ required this.backgroundLocationDenied,
required this.notificationsEnabled,
required this.internetAvailable,
});
@@ -46,6 +54,8 @@ class StandardsState {
int? maxAdvDataLen,
bool? btEnabled,
bool? locationEnabled,
+ bool? backgroundLocationEnabled,
+ bool? backgroundLocationDenied,
bool? notificationsEnabled,
bool? internetAvailable,
}) =>
@@ -59,6 +69,10 @@ class StandardsState {
maxAdvDataLen: maxAdvDataLen ?? this.maxAdvDataLen,
btEnabled: btEnabled ?? this.btEnabled,
locationEnabled: locationEnabled ?? this.locationEnabled,
+ backgroundLocationDenied:
+ backgroundLocationDenied ?? this.backgroundLocationDenied,
+ backgroundLocationEnabled:
+ backgroundLocationEnabled ?? this.backgroundLocationEnabled,
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
internetAvailable: internetAvailable ?? this.internetAvailable,
);
@@ -79,6 +93,8 @@ class StandardsCubit extends Cubit {
internetAvailable: false,
notificationsEnabled: false,
locationEnabled: false,
+ backgroundLocationEnabled: false,
+ backgroundLocationDenied: false,
),
);
@@ -128,7 +144,14 @@ class StandardsCubit extends Cubit {
);
final preferences = await SharedPreferences.getInstance();
- final logPlatformStatus = preferences.getBool('logPlatformStandards');
+
+ final backgroundLocationDenied =
+ preferences.getBool(StandardsState.backgroundLocationDeniedKey) ??
+ false;
+ emit(state.copyWith(backgroundLocationDenied: backgroundLocationDenied));
+
+ final logPlatformStatus =
+ preferences.getBool(StandardsState.logPlatformStandardsKey);
// skip if info was already logged
if (logPlatformStatus != null && !logPlatformStatus) {
return;
@@ -159,22 +182,28 @@ class StandardsCubit extends Cubit {
Logger.root.info('maxAdvDataLen $maxAdvDataLen');
// set that platforms standards were already logged
- await preferences.setBool('logPlatformStandards', true);
+ await preferences.setBool(StandardsState.logPlatformStandardsKey, true);
}
- Future setLocationEnabled({required bool enabled}) async {
- emit(state.copyWith(locationEnabled: enabled));
- }
+ void setLocationEnabled({required bool enabled}) =>
+ emit(state.copyWith(locationEnabled: enabled));
- Future setBluetoothEnabled({required bool enabled}) async {
- emit(state.copyWith(btEnabled: enabled));
- }
+ void setBackgroundLocationEnabled({required bool enabled}) async =>
+ emit(state.copyWith(backgroundLocationEnabled: enabled));
- Future setNotificationsEnabled({required bool enabled}) async {
- emit(state.copyWith(notificationsEnabled: enabled));
+ Future setBackgroundLocationDenied() async {
+ final preferences = await SharedPreferences.getInstance();
+ preferences.setBool(StandardsState.backgroundLocationDeniedKey, true);
+ emit(state.copyWith(
+ backgroundLocationEnabled: false, backgroundLocationDenied: true));
}
- Future setInternetAvailable({required bool available}) async {
- emit(state.copyWith(internetAvailable: available));
- }
+ void setBluetoothEnabled({required bool enabled}) =>
+ emit(state.copyWith(btEnabled: enabled));
+
+ void setNotificationsEnabled({required bool enabled}) =>
+ emit(state.copyWith(notificationsEnabled: enabled));
+
+ void setInternetAvailable({required bool available}) =>
+ emit(state.copyWith(internetAvailable: available));
}
diff --git a/lib/widgets/app/dialogs.dart b/lib/widgets/app/dialogs.dart
index 1ce05c23..77b3d3b3 100644
--- a/lib/widgets/app/dialogs.dart
+++ b/lib/widgets/app/dialogs.dart
@@ -1,3 +1,5 @@
+import 'dart:io';
+
import 'package:another_flushbar/flushbar.dart';
import 'package:app_settings/app_settings.dart';
import 'package:flutter/material.dart';
@@ -48,25 +50,7 @@ Future showLocationPermissionDialog({
required bool showWhileUsingPermissionExplanation,
}) async {
// set up the buttons
- final Widget cancelButton = TextButton(
- child: const Text('Cancel'),
- onPressed: () {
- Navigator.pop(context, false);
- },
- );
- final Widget appSettingsButton = TextButton(
- child: const Text('App settings'),
- onPressed: () {
- Navigator.pop(context, false);
- AppSettings.openAppSettings();
- },
- );
- final Widget continueButton = TextButton(
- child: const Text('Continue'),
- onPressed: () {
- Navigator.pop(context, true);
- },
- );
+ final actions = _getPermissionDialogActions(context);
// set up the AlertDialog
final alert = AlertDialog(
title: const Text('Location permission required'),
@@ -82,7 +66,8 @@ Future showLocationPermissionDialog({
style: TextStyle(fontWeight: FontWeight.w700),
),
const TextSpan(
- text: 'option to enable scans in the background.\n\n',
+ text:
+ 'option to enable scans while the app is in foreground.\n\n',
),
],
const TextSpan(
@@ -96,10 +81,49 @@ Future showLocationPermissionDialog({
],
),
),
+ actions: actions.values.toList(),
+ );
+ // show the dialog
+ final result = await showDialog(
+ context: context,
+ builder: (context) {
+ return alert;
+ },
+ );
+ return result ?? false;
+}
+
+Future showBackgroundPermissionDialog({
+ required BuildContext context,
+}) async {
+ // set up the buttons
+ final actions = _getPermissionDialogActions(context);
+ final isAndroid = Platform.isAndroid;
+ // set up the AlertDialog
+ final alert = AlertDialog(
+ title: const Text('Background location permission'),
+ content: const Text.rich(
+ TextSpan(
+ text: 'Drone Scanner requires background location permission to scan '
+ 'for nearby aircraft while in the background.\n\n',
+ children: [
+ TextSpan(
+ text: 'If you wish to use the Drone Radar feature or gather data '
+ 'while the app is minimized, please select\nthe '),
+ TextSpan(
+ text: '"Allow all the time"\n',
+ style: TextStyle(fontWeight: FontWeight.w700),
+ ),
+ TextSpan(
+ text: 'option in application settings.\n\n',
+ ),
+ ],
+ ),
+ ),
actions: [
- cancelButton,
- appSettingsButton,
- continueButton,
+ actions['cancel']!,
+ if (isAndroid) actions['continue']!,
+ if (!isAndroid) actions['appSettings']!,
],
);
// show the dialog
@@ -112,6 +136,30 @@ Future showLocationPermissionDialog({
return result ?? false;
}
+Map _getPermissionDialogActions(BuildContext context) {
+ return {
+ 'cancel': TextButton(
+ child: const Text('Cancel'),
+ onPressed: () {
+ Navigator.pop(context, false);
+ },
+ ),
+ 'appSettings': TextButton(
+ child: const Text('App settings'),
+ onPressed: () {
+ Navigator.pop(context, true);
+ AppSettings.openAppSettings();
+ },
+ ),
+ 'continue': TextButton(
+ child: const Text('Continue'),
+ onPressed: () {
+ Navigator.pop(context, true);
+ },
+ ),
+ };
+}
+
void showSnackBar(
BuildContext context,
String snackBarText, {
diff --git a/lib/widgets/app/life_cycle_manager.dart b/lib/widgets/app/life_cycle_manager.dart
index a9980229..bc277384 100644
--- a/lib/widgets/app/life_cycle_manager.dart
+++ b/lib/widgets/app/life_cycle_manager.dart
@@ -32,8 +32,8 @@ class _LifeCycleManagerState extends State
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
- checkPermissions();
- Future.delayed(const Duration(seconds: 5), checkInternetConnection);
+ _checkPermissions();
+ Future.delayed(const Duration(seconds: 5), _checkInternetConnection);
final alertsState = context.read().state;
if (alertsState.proximityAlertActive && alertsState.hasRecentAlerts()) {
Future.delayed(const Duration(seconds: 1),
@@ -53,13 +53,13 @@ class _LifeCycleManagerState extends State
showcaseState.showcaseActive) {
showcaseSub = context.read().stream.listen((event) {
if (event is ShowcaseStateInitialized && !event.showcaseActive) {
- initPlatformState();
+ _initPlatformState();
showcaseSub?.cancel();
}
});
return;
} else {
- initPlatformState();
+ _initPlatformState();
}
},
);
@@ -70,33 +70,24 @@ class _LifeCycleManagerState extends State
super.didChangeDependencies();
}
- Future initPlatformState() async {
- if (Platform.isAndroid) {
- await _initPermissionsAndroid(context);
- } else if (Platform.isIOS) {
- await _initPermissionsIOS(context);
- } else {
- return;
- }
- if (!mounted) return;
- if (context.read().state.showcaseActive) {
- await context.read().stop();
- }
- // check internet connection
- try {
- final result = await InternetAddress.lookup('example.com');
- if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
- if (!mounted) return;
- await context
- .read()
- .setInternetAvailable(available: true);
- }
- } on SocketException catch (_) {
- if (!mounted) return;
- await context
- .read()
- .setInternetAvailable(available: false);
- }
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance.addObserver(this);
+ }
+
+ @override
+ void dispose() {
+ WidgetsBinding.instance.removeObserver(this);
+ showcaseSub?.cancel();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ child: widget.child,
+ );
}
Future _initPermissionsIOS(BuildContext context) async {
@@ -106,7 +97,7 @@ class _LifeCycleManagerState extends State
if (btStatus.isGranted) {
if (!mounted) return;
- await standardsCubit.setBluetoothEnabled(enabled: true);
+ standardsCubit.setBluetoothEnabled(enabled: true);
if (!mounted) return;
final btTurnedOn = await odidCubit.isBtTurnedOn();
@@ -115,22 +106,22 @@ class _LifeCycleManagerState extends State
}
} else {
if (!mounted) return;
- await standardsCubit.setBluetoothEnabled(enabled: false);
+ standardsCubit.setBluetoothEnabled(enabled: false);
}
final status = await Permission.location.request();
if (status.isGranted) {
- initLocation();
+ _initLocation();
if (!mounted) return;
- await standardsCubit.setLocationEnabled(enabled: true);
+ standardsCubit.setLocationEnabled(enabled: true);
} else {
if (!mounted) return;
- await standardsCubit.setLocationEnabled(enabled: false);
+ standardsCubit.setLocationEnabled(enabled: false);
}
final notificationStatus = await Permission.notification.request();
if (notificationStatus.isGranted) {
- await standardsCubit.setNotificationsEnabled(enabled: true);
+ standardsCubit.setNotificationsEnabled(enabled: true);
}
}
@@ -138,40 +129,40 @@ class _LifeCycleManagerState extends State
final odidCubit = context.read();
final standardsCubit = context.read();
- final version = await getAndroidVersionNumber();
+ final version = await _getAndroidVersionNumber();
if (version == null) return;
final locStatus = await Permission.location.status;
// show dialog before asking for location
// when already granted or pernamently denied, request is not needed
- if (!(locStatus.isGranted || locStatus.isPermanentlyDenied) &&
- context.mounted) {
+ if (!(locStatus.isGranted) && context.mounted) {
if (await showLocationPermissionDialog(
context: context,
showWhileUsingPermissionExplanation: version >= 11,
)) {
final status = await Permission.location.request();
if (status.isDenied) {
- await standardsCubit.setLocationEnabled(enabled: false);
+ standardsCubit.setLocationEnabled(enabled: false);
} else {
- initLocation();
- await standardsCubit.setLocationEnabled(enabled: true);
+ _initLocation();
+ standardsCubit.setLocationEnabled(enabled: true);
}
}
} else if (locStatus.isGranted) {
- initLocation();
- await standardsCubit.setLocationEnabled(enabled: true);
+ _initLocation();
+ standardsCubit.setLocationEnabled(enabled: true);
}
+
final btStatus = await Permission.bluetooth.request();
// scan makes sense just on android
final btScanStatus = await Permission.bluetoothScan.request();
if (btStatus.isGranted && btScanStatus.isGranted) {
- await standardsCubit.setBluetoothEnabled(enabled: true);
+ standardsCubit.setBluetoothEnabled(enabled: true);
final btTurnedOn = await odidCubit.isBtTurnedOn();
if (btTurnedOn) {
await odidCubit.setBtUsed(btUsed: true);
}
} else {
- await standardsCubit.setBluetoothEnabled(enabled: false);
+ standardsCubit.setBluetoothEnabled(enabled: false);
}
if (!mounted) {
return;
@@ -188,44 +179,100 @@ class _LifeCycleManagerState extends State
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestPermission();
- await standardsCubit.setNotificationsEnabled(enabled: result ?? false);
+ standardsCubit.setNotificationsEnabled(enabled: result ?? false);
+ }
+
+ Future _initBackgroundLocationPermission(BuildContext context) async {
+ final standardsCubit = context.read();
+ final locStatus = await Permission.location.status;
+
+ final backgroundLocStatus = await Permission.locationAlways.status;
+ // do not ask for locationAlways if location is not granted
+ if (!locStatus.isGranted) return;
+ if ((backgroundLocStatus.isGranted)) {
+ standardsCubit.setBackgroundLocationEnabled(enabled: true);
+ return;
+ }
+
+ if (context.mounted && !standardsCubit.state.backgroundLocationDenied) {
+ // show dialog to ask user for background location permission
+ if (await showBackgroundPermissionDialog(
+ context: context,
+ )) {
+ final status = await Permission.locationAlways.request();
+
+ if (status.isGranted) {
+ if (!mounted) return;
+ standardsCubit.setBackgroundLocationEnabled(enabled: true);
+ } else {
+ if (!mounted) return;
+ standardsCubit.setBackgroundLocationEnabled(enabled: false);
+ }
+ }
+ // user denied background loc -> save response and do not ask again
+ else {
+ await standardsCubit.setBackgroundLocationDenied();
+ }
+ }
+ }
+
+ Future _initPlatformState() async {
+ if (Platform.isAndroid) {
+ await _initPermissionsAndroid(context);
+ } else if (Platform.isIOS) {
+ await _initPermissionsIOS(context);
+ } else {
+ return;
+ }
+
+ if (!mounted) return;
+
+ await _initBackgroundLocationPermission(context);
+
+ if (!mounted) return;
+ if (context.read().state.showcaseActive) {
+ await context.read().stop();
+ }
+ _checkInternetConnection();
}
- Future getAndroidVersionNumber() async {
+ Future _getAndroidVersionNumber() async {
final deviceInfo = DeviceInfoPlugin();
final androidVersion = (await deviceInfo.androidInfo).version.release;
return int.tryParse(androidVersion);
}
- void initLocation() {
+ void _initLocation() {
final location = Location();
- location.getLocation().then(userLocationChanged);
- location.onLocationChanged.listen(userLocationChanged);
+ location.getLocation().then(_userLocationChanged);
+ location.onLocationChanged.listen(_userLocationChanged);
}
// check permission status without requests
- Future checkPermissions() async {
+ Future _checkPermissions() async {
final standardsCubit = context.read();
if (!mounted) return;
final loc = await Permission.location.isGranted;
// check loc, if was not set before, init listener
if (loc && !standardsCubit.state.locationEnabled) {
- initLocation();
+ _initLocation();
}
- await standardsCubit.setLocationEnabled(enabled: loc);
+ standardsCubit.setLocationEnabled(enabled: loc);
+ final backgroundLoc = await Permission.locationAlways.isGranted;
+ standardsCubit.setBackgroundLocationEnabled(enabled: backgroundLoc);
final bt = await Permission.bluetooth.isGranted;
if (Platform.isAndroid) {
final btScan = await Permission.bluetoothScan.isGranted;
- await standardsCubit.setBluetoothEnabled(enabled: bt && btScan);
+ standardsCubit.setBluetoothEnabled(enabled: bt && btScan);
} else {
- await standardsCubit.setBluetoothEnabled(enabled: bt);
+ standardsCubit.setBluetoothEnabled(enabled: bt);
}
- await standardsCubit.setNotificationsEnabled(
+ standardsCubit.setNotificationsEnabled(
enabled: await Permission.notification.isGranted);
}
- void userLocationChanged(LocationData currentLocation) {
+ void _userLocationChanged(LocationData currentLocation) {
void updateLoc() {
final mapCubit = context.read();
if (currentLocation.latitude != null &&
@@ -249,42 +296,16 @@ class _LifeCycleManagerState extends State
}
}
- Future checkInternetConnection() async {
+ Future _checkInternetConnection() async {
// check internet
+ final standardsCubit = context.read();
try {
final result = await InternetAddress.lookup('example.com');
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
- if (!mounted) return;
- await context
- .read()
- .setInternetAvailable(available: true);
+ standardsCubit.setInternetAvailable(available: true);
}
} on SocketException catch (_) {
- if (mounted) {
- await context
- .read()
- .setInternetAvailable(available: false);
- }
+ standardsCubit.setInternetAvailable(available: false);
}
}
-
- @override
- void initState() {
- super.initState();
- WidgetsBinding.instance.addObserver(this);
- }
-
- @override
- void dispose() {
- WidgetsBinding.instance.removeObserver(this);
- showcaseSub?.cancel();
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- return Container(
- child: widget.child,
- );
- }
}
diff --git a/lib/widgets/preferences/preferences_page.dart b/lib/widgets/preferences/preferences_page.dart
index 034f1c25..48a7df72 100644
--- a/lib/widgets/preferences/preferences_page.dart
+++ b/lib/widgets/preferences/preferences_page.dart
@@ -319,6 +319,16 @@ class PreferencesPage extends StatelessWidget {
icon: state.locationEnabled ? positiveIcon : negativeIcon,
),
),
+ Padding(
+ padding: itemPadding,
+ child: PreferencesField(
+ label: 'Background Location',
+ text: state.backgroundLocationEnabled ? 'Granted' : 'Not Granted',
+ color:
+ state.backgroundLocationEnabled ? AppColors.green : AppColors.red,
+ icon: state.backgroundLocationEnabled ? positiveIcon : negativeIcon,
+ ),
+ ),
Padding(
padding: itemPadding,
child: PreferencesField(