fix: permission screen - location tap opens settings when permanently denied, battery dialog re-checks on done

This commit is contained in:
fiatcode 2026-04-30 16:21:07 +07:00
parent 650a6efeca
commit 6cbb7a2070
No known key found for this signature in database
3 changed files with 83 additions and 38 deletions

View file

@ -90,6 +90,9 @@ class BridgeModule : FlutterPlugin, MethodChannel.MethodCallHandler {
"openBatteryOptimizationSettings" -> { "openBatteryOptimizationSettings" -> {
openBatteryOptimizationSettings(result) openBatteryOptimizationSettings(result)
} }
"isBatteryOptimizationDisabled" -> {
result.success(isBatteryOptimizationDisabled())
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@ -215,4 +218,14 @@ class BridgeModule : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
} }
} }
private fun isBatteryOptimizationDisabled(): Boolean {
return try {
val powerManager = context?.getSystemService(android.content.Context.POWER_SERVICE) as? android.os.PowerManager
val packageName = context?.packageName ?: ""
powerManager?.isIgnoringBatteryOptimizations(packageName) == true
} catch (e: Exception) {
false
}
}
} }

View file

@ -104,4 +104,16 @@ class LocationBridge {
return false; return false;
} }
} }
static Future<bool> isBatteryOptimizationDisabled() async {
try {
final result = await _methodChannel.invokeMethod<bool>(
'isBatteryOptimizationDisabled',
);
return result ?? false;
} on PlatformException catch (e) {
debugPrint('Failed to check battery optimization: ${e.message}');
return false;
}
}
} }

View file

@ -32,7 +32,6 @@ class _PermissionScreenState extends State<PermissionScreen> {
Future<void> _checkPermissions() async { Future<void> _checkPermissions() async {
final location = await Permission.locationAlways.status; final location = await Permission.locationAlways.status;
final notification = await Permission.notification.status; final notification = await Permission.notification.status;
// Battery optimization check is Android-only
final batteryOptOut = await _isBatteryOptimizationDisabled(); final batteryOptOut = await _isBatteryOptimizationDisabled();
if (mounted) { if (mounted) {
@ -45,43 +44,52 @@ class _PermissionScreenState extends State<PermissionScreen> {
} }
Future<bool> _isBatteryOptimizationDisabled() async { Future<bool> _isBatteryOptimizationDisabled() async {
// On Android, check if battery optimization is disabled for the app try {
// We approximate this - in production you'd use a native method final result = await LocationBridge.isBatteryOptimizationDisabled();
// For now, assume not granted until user says so return result == true;
} catch (_) {
return false; return false;
} }
}
bool get _allGranted => bool get _allGranted =>
_locationGranted && _notificationGranted && _batteryOptOut; _locationGranted && _notificationGranted && _batteryOptOut;
Future<void> _requestLocation() async { Future<void> _requestLocation() async {
final status = await Permission.locationAlways.request(); final status = await Permission.locationAlways.request();
if (!status.isGranted && status.isPermanentlyDenied) {
// Permission permanently denied - open app settings
await openAppSettings();
}
if (mounted) { if (mounted) {
setState(() => _locationGranted = status.isGranted); final newStatus = await Permission.locationAlways.status;
setState(() => _locationGranted = newStatus.isGranted);
} }
} }
Future<void> _requestNotification() async { Future<void> _requestNotification() async {
final status = await Permission.notification.request(); final status = await Permission.notification.request();
if (!status.isGranted && status.isPermanentlyDenied) {
await openAppSettings();
}
if (mounted) { if (mounted) {
setState(() => _notificationGranted = status.isGranted); final newStatus = await Permission.notification.status;
setState(() => _notificationGranted = newStatus.isGranted);
} }
} }
Future<void> _requestBatteryOptOut() async { Future<void> _requestBatteryOptOut() async {
final success = await LocationBridge.openBatteryOptimizationSettings(); final opened = await LocationBridge.openBatteryOptimizationSettings();
if (success) { if (mounted) {
// Show dialog asking user to confirm after disabling battery opt // Show dialog after the settings screen opens
if (mounted) _showBatteryOptDialog(); _showBatteryOptDialog(opened);
} else {
// Fallback to manual instructions dialog
if (mounted) _showBatteryOptDialog();
} }
} }
void _showBatteryOptDialog() { void _showBatteryOptDialog(bool settingsOpened) {
showDialog( showDialog(
context: context, context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF1a1a1a), backgroundColor: const Color(0xFF1a1a1a),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@ -98,10 +106,12 @@ class _PermissionScreenState extends State<PermissionScreen> {
color: Color(0xFFe0e0e0), color: Color(0xFFe0e0e0),
), ),
), ),
content: const Text( content: Text(
'Please disable battery optimization for this app to ensure reliable background tracking.\n\n' settingsOpened
'In the next screen, find "Traccar Client" and set it to "Don\'t optimize" or "Unrestricted".', ? 'A settings screen has opened.\n\n'
style: TextStyle( 'Find "Traccar Client" and set it to "Don\'t optimize" or "Unrestricted", then come back and tap DONE below.'
: 'Open Settings → Apps → Traccar Client → Battery and select "Don\'t optimize" or "Unrestricted", then come back and tap DONE below.',
style: const TextStyle(
fontFamily: 'monospace', fontFamily: 'monospace',
fontSize: 12, fontSize: 12,
color: Color(0xFF9e9e9e), color: Color(0xFF9e9e9e),
@ -109,9 +119,13 @@ class _PermissionScreenState extends State<PermissionScreen> {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(ctx), onPressed: () {
Navigator.pop(ctx);
// Open app details settings as fallback
openAppSettings();
},
child: const Text( child: const Text(
'OPEN SETTINGS', 'OPEN APP SETTINGS',
style: TextStyle( style: TextStyle(
fontFamily: 'monospace', fontFamily: 'monospace',
fontSize: 11, fontSize: 11,
@ -122,12 +136,16 @@ class _PermissionScreenState extends State<PermissionScreen> {
), ),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () async {
Navigator.pop(ctx); Navigator.pop(ctx);
setState(() => _batteryOptOut = true); // Re-check battery optimization state via native
final disabled = await _isBatteryOptimizationDisabled();
if (mounted) {
setState(() => _batteryOptOut = disabled);
}
}, },
child: const Text( child: const Text(
'DONE', 'CHECK & DONE',
style: TextStyle( style: TextStyle(
fontFamily: 'monospace', fontFamily: 'monospace',
fontSize: 11, fontSize: 11,
@ -213,11 +231,11 @@ class _PermissionScreenState extends State<PermissionScreen> {
color: Color(0xFF161616), color: Color(0xFF161616),
border: Border(bottom: BorderSide(color: Color(0xFF2a2a2a), width: 1)), border: Border(bottom: BorderSide(color: Color(0xFF2a2a2a), width: 1)),
), ),
child: Row( child: const Row(
children: [ children: [
const Icon(Icons.shield, color: Color(0xFF00bcd4), size: 20), Icon(Icons.shield, color: Color(0xFF00bcd4), size: 20),
const SizedBox(width: 12), SizedBox(width: 12),
const Text( Text(
'PERMISSIONS', 'PERMISSIONS',
style: TextStyle( style: TextStyle(
fontFamily: 'monospace', fontFamily: 'monospace',
@ -245,18 +263,18 @@ class _PermissionScreenState extends State<PermissionScreen> {
Container( Container(
width: 40, width: 40,
height: 40, height: 40,
decoration: BoxDecoration( decoration: const BoxDecoration(
color: const Color(0xFF00bcd4).withOpacity(0.15), color: Color(0xFF00bcd4),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.all(Radius.circular(4)),
), ),
child: const Icon( child: const Icon(
Icons.info_outline, Icons.info_outline,
color: Color(0xFF00bcd4), color: Colors.white,
size: 20, size: 20,
), ),
), ),
const SizedBox(width: 14), SizedBox(width: 14),
const Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -420,7 +438,7 @@ class _PermissionScreenState extends State<PermissionScreen> {
_notificationGranted, _notificationGranted,
_batteryOptOut, _batteryOptOut,
].where((x) => x).length; ].where((x) => x).length;
final total = 3; const total = 3;
final progress = granted / total; final progress = granted / total;
return Column( return Column(
@ -428,9 +446,11 @@ class _PermissionScreenState extends State<PermissionScreen> {
children: [ children: [
Row( Row(
children: [ children: [
const Icon( Icon(
Icons.check_circle_outline, granted == total
color: Color(0xFF00e676), ? Icons.check_circle
: Icons.check_circle_outline,
color: const Color(0xFF00e676),
size: 14, size: 14,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),