551 lines
17 KiB
Dart
551 lines
17 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
|
|
import 'bridge/location_bridge.dart';
|
|
import 'main_screen.dart';
|
|
import 'preferences.dart';
|
|
|
|
class PermissionScreen extends StatefulWidget {
|
|
const PermissionScreen({super.key});
|
|
|
|
@override
|
|
State<PermissionScreen> createState() => _PermissionScreenState();
|
|
}
|
|
|
|
class _PermissionScreenState extends State<PermissionScreen> {
|
|
bool _locationGranted = false;
|
|
bool _notificationGranted = false;
|
|
bool _batteryOptOut = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
SystemChrome.setSystemUIOverlayStyle(
|
|
const SystemUiOverlayStyle(
|
|
statusBarColor: Color(0xFF0d0d0d),
|
|
statusBarIconBrightness: Brightness.light,
|
|
),
|
|
);
|
|
_checkPermissions();
|
|
}
|
|
|
|
Future<void> _checkPermissions() async {
|
|
final location = await Permission.locationAlways.status;
|
|
final notification = await Permission.notification.status;
|
|
final batteryOptOut = await _isBatteryOptimizationDisabled();
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_locationGranted = location.isGranted;
|
|
_notificationGranted = notification.isGranted;
|
|
_batteryOptOut = batteryOptOut;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<bool> _isBatteryOptimizationDisabled() async {
|
|
try {
|
|
final result = await LocationBridge.isBatteryOptimizationDisabled();
|
|
return result == true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool get _allGranted =>
|
|
_locationGranted && _notificationGranted && _batteryOptOut;
|
|
|
|
Future<void> _requestLocation() async {
|
|
// Check current status first
|
|
var status = await Permission.locationAlways.status;
|
|
if (status.isGranted) {
|
|
if (mounted) setState(() => _locationGranted = true);
|
|
return;
|
|
}
|
|
|
|
// On Android, request locationWhenInUse first before locationAlways
|
|
var whenInUse = await Permission.locationWhenInUse.status;
|
|
if (!whenInUse.isGranted) {
|
|
whenInUse = await Permission.locationWhenInUse.request();
|
|
}
|
|
|
|
// Now request locationAlways (background permission)
|
|
status = await Permission.locationAlways.request();
|
|
|
|
if (!status.isGranted && status.isPermanentlyDenied) {
|
|
await openAppSettings();
|
|
}
|
|
|
|
final finalStatus = await Permission.locationAlways.status;
|
|
if (mounted) {
|
|
setState(() => _locationGranted = finalStatus.isGranted);
|
|
}
|
|
}
|
|
|
|
Future<void> _requestNotification() async {
|
|
var status = await Permission.notification.status;
|
|
if (status.isGranted) {
|
|
if (mounted) setState(() => _notificationGranted = true);
|
|
return;
|
|
}
|
|
|
|
status = await Permission.notification.request();
|
|
|
|
if (!status.isGranted && status.isPermanentlyDenied) {
|
|
await openAppSettings();
|
|
}
|
|
|
|
final finalStatus = await Permission.notification.status;
|
|
if (mounted) {
|
|
setState(() => _notificationGranted = finalStatus.isGranted);
|
|
}
|
|
}
|
|
|
|
Future<void> _requestBatteryOptOut() async {
|
|
final opened = await LocationBridge.openBatteryOptimizationSettings();
|
|
if (mounted) {
|
|
_showBatteryOptDialog(opened);
|
|
}
|
|
}
|
|
|
|
void _showBatteryOptDialog(bool settingsOpened) {
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (ctx) => AlertDialog(
|
|
backgroundColor: const Color(0xFF1a1a1a),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(4),
|
|
side: const BorderSide(color: Color(0xFF2a2a2a)),
|
|
),
|
|
title: const Text(
|
|
'BATTERY OPTIMIZATION',
|
|
style: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 2,
|
|
color: Color(0xFFe0e0e0),
|
|
),
|
|
),
|
|
content: Text(
|
|
settingsOpened
|
|
? 'A settings screen has opened.\n\n'
|
|
'Find "TracPulse" and set it to "Don\'t optimize" or "Unrestricted", then come back and tap CHECK & DONE.'
|
|
: 'Open Settings → Apps → TracPulse → Battery and select "Don\'t optimize" or "Unrestricted", then come back and tap CHECK & DONE.',
|
|
style: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 12,
|
|
color: Color(0xFF9e9e9e),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.pop(ctx);
|
|
openAppSettings();
|
|
},
|
|
child: const Text(
|
|
'OPEN APP SETTINGS',
|
|
style: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1,
|
|
color: Color(0xFF00bcd4),
|
|
),
|
|
),
|
|
),
|
|
TextButton(
|
|
onPressed: () async {
|
|
Navigator.pop(ctx);
|
|
final disabled = await _isBatteryOptimizationDisabled();
|
|
if (mounted) {
|
|
setState(() => _batteryOptOut = disabled);
|
|
}
|
|
},
|
|
child: const Text(
|
|
'CHECK & DONE',
|
|
style: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1,
|
|
color: Color(0xFF00e676),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _complete() async {
|
|
await Preferences.setPermissionsGranted(true);
|
|
if (mounted) {
|
|
Navigator.pushReplacement(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const MainScreen()),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFF0d0d0d),
|
|
body: SafeArea(
|
|
child: Column(
|
|
children: [
|
|
_buildHeader(),
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildIntroCard(),
|
|
const SizedBox(height: 20),
|
|
_buildPermissionItem(
|
|
icon: Icons.location_on,
|
|
title: 'LOCATION',
|
|
subtitle: 'All the time access',
|
|
granted: _locationGranted,
|
|
onRequest: _requestLocation,
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildPermissionItem(
|
|
icon: Icons.notifications,
|
|
title: 'NOTIFICATION',
|
|
subtitle: 'Show sync & tracking alerts',
|
|
granted: _notificationGranted,
|
|
onRequest: _requestNotification,
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildPermissionItem(
|
|
icon: Icons.battery_full,
|
|
title: 'BATTERY',
|
|
subtitle: 'Disable optimization for reliability',
|
|
granted: _batteryOptOut,
|
|
onRequest: _requestBatteryOptOut,
|
|
),
|
|
const SizedBox(height: 32),
|
|
_buildStatusSummary(),
|
|
const SizedBox(height: 24),
|
|
_buildContinueButton(),
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader() {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF161616),
|
|
border: Border(bottom: BorderSide(color: Color(0xFF2a2a2a), width: 1)),
|
|
),
|
|
child: const Row(
|
|
children: [
|
|
Icon(Icons.shield, color: Color(0xFF00bcd4), size: 20),
|
|
SizedBox(width: 12),
|
|
Text(
|
|
'PERMISSIONS',
|
|
style: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 3,
|
|
color: Color(0xFFe0e0e0),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildIntroCard() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF161616),
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(color: const Color(0xFF2a2a2a), width: 1),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF00bcd4),
|
|
borderRadius: BorderRadius.all(Radius.circular(4)),
|
|
),
|
|
child: const Icon(
|
|
Icons.info_outline,
|
|
color: Colors.white,
|
|
size: 20,
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
const Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Required Permissions',
|
|
style: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1,
|
|
color: Color(0xFFe0e0e0),
|
|
),
|
|
),
|
|
SizedBox(height: 4),
|
|
Text(
|
|
'TracPulse needs these permissions to track your location reliably in the background.',
|
|
style: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 11,
|
|
color: Color(0xFF757575),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPermissionItem({
|
|
required IconData icon,
|
|
required String title,
|
|
required String subtitle,
|
|
required bool granted,
|
|
required VoidCallback onRequest,
|
|
}) {
|
|
return Material(
|
|
color: const Color(0xFF161616),
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: InkWell(
|
|
onTap: granted ? null : onRequest,
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(
|
|
color: granted
|
|
? const Color(0xFF00e676).withValues(alpha: 0.3)
|
|
: const Color(0xFF2a2a2a),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 36,
|
|
height: 36,
|
|
decoration: BoxDecoration(
|
|
color: granted
|
|
? const Color(0xFF00e676).withValues(alpha: 0.15)
|
|
: const Color(0xFF2a2a2a),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
color: granted
|
|
? const Color(0xFF00e676)
|
|
: const Color(0xFF616161),
|
|
size: 18,
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1,
|
|
color: Color(0xFFe0e0e0),
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
subtitle,
|
|
style: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 10,
|
|
color: Color(0xFF616161),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (granted)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF00e676).withValues(alpha: 0.15),
|
|
borderRadius: BorderRadius.circular(3),
|
|
border: Border.all(
|
|
color: const Color(0xFF00e676).withValues(alpha: 0.4),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: const Text(
|
|
'GRANTED',
|
|
style: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1,
|
|
color: Color(0xFF00e676),
|
|
),
|
|
),
|
|
)
|
|
else
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF00bcd4).withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(3),
|
|
border: Border.all(
|
|
color: const Color(0xFF00bcd4).withValues(alpha: 0.4),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: const Text(
|
|
'TAP TO GRANT',
|
|
style: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1,
|
|
color: Color(0xFF00bcd4),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatusSummary() {
|
|
final granted = [
|
|
_locationGranted,
|
|
_notificationGranted,
|
|
_batteryOptOut,
|
|
].where((x) => x).length;
|
|
const total = 3;
|
|
final progress = granted / total;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
granted == total
|
|
? Icons.check_circle
|
|
: Icons.check_circle_outline,
|
|
color: const Color(0xFF00e676),
|
|
size: 14,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'$granted / $total GRANTED',
|
|
style: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1,
|
|
color: Color(0xFF00e676),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
Container(
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF2a2a2a),
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
child: FractionallySizedBox(
|
|
alignment: Alignment.centerLeft,
|
|
widthFactor: progress,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF00e676),
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildContinueButton() {
|
|
return Material(
|
|
color: _allGranted
|
|
? const Color(0xFF00e676).withValues(alpha: 0.15)
|
|
: const Color(0xFF2a2a2a),
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: InkWell(
|
|
onTap: _allGranted ? _complete : null,
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(
|
|
color: _allGranted
|
|
? const Color(0xFF00e676).withValues(alpha: 0.4)
|
|
: const Color(0xFF2a2a2a),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
_allGranted ? 'CONTINUE' : 'GRANT ALL PERMISSIONS',
|
|
style: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 2,
|
|
color: _allGranted
|
|
? const Color(0xFF00e676)
|
|
: const Color(0xFF616161),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|