import 'package:flutter/material.dart'; import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:permission_handler/permission_handler.dart'; import '../../../core/providers/bt_provider.dart'; import '../../widgets/main_nav.dart'; import '../../../core/logging/app_log.dart'; class BtPickerScreen extends ConsumerStatefulWidget { const BtPickerScreen({super.key}); @override ConsumerState createState() => _BtPickerScreenState(); } class _BtPickerScreenState extends ConsumerState { String? _error; bool _initDone = false; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => _initBluetooth()); } bool _permissionsOk = false; Future _initBluetooth() async { setState(() { _error = null; _initDone = false; }); try { // Android 12+: BLUETOOTH_CONNECT is the only permission needed to list // paired devices and open an RFCOMM socket. On Android 11 and older // permission_handler reports it as granted. Scan/location are requested // but not required (we never run discovery). final statuses = await [ Permission.bluetoothConnect, Permission.bluetoothScan, Permission.locationWhenInUse, ].request(); final connect = statuses[Permission.bluetoothConnect]; if (connect != null && !connect.isGranted && !connect.isLimited) { AppLog.w('Bluetooth permission not granted: $connect'); if (!mounted) return; setState(() { _permissionsOk = false; _error = 'Bluetooth permission ("Nearby devices") was not granted.\n\n' 'Go to Settings → Apps → HV BT Dashboard → Permissions.'; _initDone = true; }); if (connect.isPermanentlyDenied) await openAppSettings(); return; } _permissionsOk = true; final btState = await FlutterBluetoothSerial.instance.state; if (btState != BluetoothState.STATE_ON) { await FlutterBluetoothSerial.instance.requestEnable(); await Future.delayed(const Duration(seconds: 1)); } } catch (e, st) { AppLog.e('Bluetooth init failed', e, st); if (!mounted) return; setState(() => _error = 'Bluetooth init failed: $e'); } if (!mounted) return; setState(() => _initDone = true); if (_permissionsOk) ref.invalidate(pairedDevicesProvider); } // ECU protocol is chosen once via Settings (or defaults to S300) and // persisted from there — no need to ask again on every connection. A // dialog here used to re-prompt "S300 or KPro?" on every single tap, // which was pure friction once the user had already told the app which // ECU they have. Future _connect(BluetoothDevice device) async { if (ref.read(btProvider).isConnecting) return; await ref.read(btProvider.notifier).connect(device); if (!mounted) return; if (ref.read(btProvider).isConnected) { Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (_) => const MainNav()), ); } } @override Widget build(BuildContext context) { final btState = ref.watch(btProvider); // Do NOT touch the paired-device list before permission is granted: // on Android 12+ that call throws inside the Bluetooth plugin. final AsyncValue> devicesAsync = _permissionsOk ? ref.watch(pairedDevicesProvider) : const AsyncValue>.data([]); return Scaffold( appBar: AppBar( title: const Text('Connect to ECU'), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: _loading ? null : _initBluetooth, ), ], ), body: Column( children: [ if (btState.status == BtStatus.error) _banner('Connection failed: ${btState.error}'), if (_error != null) _banner(_error!), Expanded( child: !_initDone ? const Center(child: CircularProgressIndicator()) : devicesAsync.when( loading: () => const Center( child: CircularProgressIndicator()), error: (e, _) => Center(child: _banner('Failed to load: $e')), data: (devices) => devices.isEmpty ? const Center( child: Padding( padding: EdgeInsets.all(24), child: Text( 'No paired BT devices.\nPair your ECU in Android Settings first.', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey), ), ), ) : ListView.separated( itemCount: devices.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (_, i) { final d = devices[i]; final connecting = btState.isConnecting && btState.deviceName == (d.name ?? d.address); return ListTile( leading: const Icon(Icons.bluetooth), title: Text(d.name ?? 'Unknown'), subtitle: Text(d.address), trailing: connecting ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2)) : const Icon( Icons.arrow_forward_ios, size: 14), onTap: connecting ? null : () => _connect(d), ); }, ), ), ), ], ), ); } bool get _loading => !_initDone || ref.read(btProvider).isConnecting; Widget _banner(String msg) => Container( width: double.infinity, color: const Color(0xFF4A0000), padding: const EdgeInsets.all(12), child: Text(msg, style: const TextStyle(color: Colors.redAccent, fontSize: 12)), ); }