diff --git a/android/app/src/main/kotlin/com/traccar/traccar_client/BridgeModule.kt b/android/app/src/main/kotlin/com/traccar/traccar_client/BridgeModule.kt index b01d784..8482411 100644 --- a/android/app/src/main/kotlin/com/traccar/traccar_client/BridgeModule.kt +++ b/android/app/src/main/kotlin/com/traccar/traccar_client/BridgeModule.kt @@ -90,6 +90,9 @@ class BridgeModule : FlutterPlugin, MethodChannel.MethodCallHandler { "openBatteryOptimizationSettings" -> { openBatteryOptimizationSettings(result) } + "isBatteryOptimizationDisabled" -> { + result.success(isBatteryOptimizationDisabled()) + } 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 + } + } } \ No newline at end of file diff --git a/lib/bridge/location_bridge.dart b/lib/bridge/location_bridge.dart index 04407a1..096257f 100644 --- a/lib/bridge/location_bridge.dart +++ b/lib/bridge/location_bridge.dart @@ -104,4 +104,16 @@ class LocationBridge { return false; } } + + static Future isBatteryOptimizationDisabled() async { + try { + final result = await _methodChannel.invokeMethod( + 'isBatteryOptimizationDisabled', + ); + return result ?? false; + } on PlatformException catch (e) { + debugPrint('Failed to check battery optimization: ${e.message}'); + return false; + } + } } diff --git a/lib/permission_screen.dart b/lib/permission_screen.dart index 4e96d8a..d5de06b 100644 --- a/lib/permission_screen.dart +++ b/lib/permission_screen.dart @@ -32,7 +32,6 @@ class _PermissionScreenState extends State { Future _checkPermissions() async { final location = await Permission.locationAlways.status; final notification = await Permission.notification.status; - // Battery optimization check is Android-only final batteryOptOut = await _isBatteryOptimizationDisabled(); if (mounted) { @@ -45,10 +44,12 @@ class _PermissionScreenState extends State { } Future _isBatteryOptimizationDisabled() async { - // On Android, check if battery optimization is disabled for the app - // We approximate this - in production you'd use a native method - // For now, assume not granted until user says so - return false; + try { + final result = await LocationBridge.isBatteryOptimizationDisabled(); + return result == true; + } catch (_) { + return false; + } } bool get _allGranted => @@ -56,32 +57,39 @@ class _PermissionScreenState extends State { Future _requestLocation() async { final status = await Permission.locationAlways.request(); + if (!status.isGranted && status.isPermanentlyDenied) { + // Permission permanently denied - open app settings + await openAppSettings(); + } if (mounted) { - setState(() => _locationGranted = status.isGranted); + final newStatus = await Permission.locationAlways.status; + setState(() => _locationGranted = newStatus.isGranted); } } Future _requestNotification() async { final status = await Permission.notification.request(); + if (!status.isGranted && status.isPermanentlyDenied) { + await openAppSettings(); + } if (mounted) { - setState(() => _notificationGranted = status.isGranted); + final newStatus = await Permission.notification.status; + setState(() => _notificationGranted = newStatus.isGranted); } } Future _requestBatteryOptOut() async { - final success = await LocationBridge.openBatteryOptimizationSettings(); - if (success) { - // Show dialog asking user to confirm after disabling battery opt - if (mounted) _showBatteryOptDialog(); - } else { - // Fallback to manual instructions dialog - if (mounted) _showBatteryOptDialog(); + final opened = await LocationBridge.openBatteryOptimizationSettings(); + if (mounted) { + // Show dialog after the settings screen opens + _showBatteryOptDialog(opened); } } - void _showBatteryOptDialog() { + void _showBatteryOptDialog(bool settingsOpened) { showDialog( context: context, + barrierDismissible: false, builder: (ctx) => AlertDialog( backgroundColor: const Color(0xFF1a1a1a), shape: RoundedRectangleBorder( @@ -98,10 +106,12 @@ class _PermissionScreenState extends State { color: Color(0xFFe0e0e0), ), ), - content: const Text( - 'Please disable battery optimization for this app to ensure reliable background tracking.\n\n' - 'In the next screen, find "Traccar Client" and set it to "Don\'t optimize" or "Unrestricted".', - style: TextStyle( + content: Text( + settingsOpened + ? 'A settings screen has opened.\n\n' + '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', fontSize: 12, color: Color(0xFF9e9e9e), @@ -109,9 +119,13 @@ class _PermissionScreenState extends State { ), actions: [ TextButton( - onPressed: () => Navigator.pop(ctx), + onPressed: () { + Navigator.pop(ctx); + // Open app details settings as fallback + openAppSettings(); + }, child: const Text( - 'OPEN SETTINGS', + 'OPEN APP SETTINGS', style: TextStyle( fontFamily: 'monospace', fontSize: 11, @@ -122,12 +136,16 @@ class _PermissionScreenState extends State { ), ), TextButton( - onPressed: () { + onPressed: () async { 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( - 'DONE', + 'CHECK & DONE', style: TextStyle( fontFamily: 'monospace', fontSize: 11, @@ -213,11 +231,11 @@ class _PermissionScreenState extends State { color: Color(0xFF161616), border: Border(bottom: BorderSide(color: Color(0xFF2a2a2a), width: 1)), ), - child: Row( + child: const Row( children: [ - const Icon(Icons.shield, color: Color(0xFF00bcd4), size: 20), - const SizedBox(width: 12), - const Text( + Icon(Icons.shield, color: Color(0xFF00bcd4), size: 20), + SizedBox(width: 12), + Text( 'PERMISSIONS', style: TextStyle( fontFamily: 'monospace', @@ -245,18 +263,18 @@ class _PermissionScreenState extends State { Container( width: 40, height: 40, - decoration: BoxDecoration( - color: const Color(0xFF00bcd4).withOpacity(0.15), - borderRadius: BorderRadius.circular(4), + decoration: const BoxDecoration( + color: Color(0xFF00bcd4), + borderRadius: BorderRadius.all(Radius.circular(4)), ), child: const Icon( Icons.info_outline, - color: Color(0xFF00bcd4), + color: Colors.white, size: 20, ), ), - const SizedBox(width: 14), - const Expanded( + SizedBox(width: 14), + Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -420,7 +438,7 @@ class _PermissionScreenState extends State { _notificationGranted, _batteryOptOut, ].where((x) => x).length; - final total = 3; + const total = 3; final progress = granted / total; return Column( @@ -428,9 +446,11 @@ class _PermissionScreenState extends State { children: [ Row( children: [ - const Icon( - Icons.check_circle_outline, - color: Color(0xFF00e676), + Icon( + granted == total + ? Icons.check_circle + : Icons.check_circle_outline, + color: const Color(0xFF00e676), size: 14, ), const SizedBox(width: 8),