import 'dart:async'; import 'dart:io'; import 'package:miler/helpers/http_overrides.dart'; import 'dart:isolate'; import 'package:flutter/foundation.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'dart:math' as math; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miler/views/helpers/constants/apiconstants.dart'; import 'package:miler/providers/Riderlog/riderlog_provider.dart'; import 'package:miler/background/backgroundservice.dart'; import 'package:geolocator/geolocator.dart'; import 'package:miler/data/device_telemetry.dart'; import 'package:miler/utils/kalman_filter.dart'; import 'package:miler/utils/mqtt_service.dart'; import 'package:miler/views/helpers/constants/mqtt_constants.dart'; class _BackgroundRiderLog { static MilerKalmanFilter? _kf; static DateTime? _lastUpdateTime; static Future> _ensureLatLng() async { Map result = { 'lat': '0', 'lng': '0', 'raw_lat': '0', 'raw_lng': '0', 'speed': '0', 'heading': '0', 'velocity_lat': '0', 'velocity_lng': '0', 'status': 'unknown', 'accuracy': '0', }; try { // 1. Check if location services are enabled final serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { debugPrint('[BG_RIDER_LOG] Location services are disabled.'); result['status'] = 'disabled'; return result; } // 2. Check permissions LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { debugPrint('[BG_RIDER_LOG] Location permission denied.'); result['status'] = 'denied'; return result; } if (permission == LocationPermission.deniedForever) { debugPrint('[BG_RIDER_LOG] Location permission denied forever.'); result['status'] = 'denied_forever'; return result; } result['status'] = 'enabled'; // 3. Get position (using non-deprecated LocationSettings + explicit timeout) final pos = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, ), ); final now = DateTime.now(); double outLat = pos.latitude; double outLng = pos.longitude; double speed = pos.speed; double heading = pos.heading; // Decompose velocity for Kalman final double headingRadians = heading * (math.pi / 180.0); final double velocityLng = speed * math.sin(headingRadians); final double velocityLat = speed * math.cos(headingRadians); if (_kf == null) { _kf = MilerKalmanFilter(lat: outLat, lng: outLng); } else { final double dt = _lastUpdateTime != null ? now.difference(_lastUpdateTime!).inMilliseconds / 1000.0 : 30.0; // Default background interval _kf!.predict(dt); _kf!.update(outLat, outLng); outLat = _kf!.x[0]; outLng = _kf!.x[1]; } _lastUpdateTime = now; return { 'lat': outLat.toStringAsFixed(6), 'lng': outLng.toStringAsFixed(6), 'raw_lat': pos.latitude.toStringAsFixed(6), 'raw_lng': pos.longitude.toStringAsFixed(6), 'speed': speed.toStringAsFixed(2), 'heading': heading.toStringAsFixed(2), 'velocity_lat': velocityLat.toStringAsFixed(4), 'velocity_lng': velocityLng.toStringAsFixed(4), 'status': 'enabled', 'accuracy': pos.accuracy.toStringAsFixed(1), }; } catch (e) { debugPrint('[BG_RIDER_LOG] Error getting location: $e'); return result; } } static String _two(int n) => n.toString().padLeft(2, '0'); static String _formatDateTimeFull(DateTime dt) { final y = dt.year.toString(); final m = _two(dt.month); final d = _two(dt.day); final hh = _two(dt.hour); final mm = _two(dt.minute); final ss = _two(dt.second); return "$y-$m-$d $hh:$mm:$ss"; } static String _formatTime(DateTime dt) { final hh = _two(dt.hour); final mm = _two(dt.minute); final ss = _two(dt.second); return "$hh:$mm:$ss"; } static Future createLoginNow() async { try { final prefs = await SharedPreferences.getInstance(); final int onduty = prefs.getInt('onduty') ?? 0; if (onduty != 1) { return; } final int? userid = prefs.getInt('userId') ?? prefs.getInt('userid'); final int? partnerid = prefs.getInt('partnerId') ?? prefs.getInt('partnerid'); final int? shiftid = prefs.getInt('shiftId') ?? prefs.getInt('shiftid'); if ((userid ?? 0) == 0) return; // Prefer explicit username, then fallback to stored full name or first/last String? username = prefs.getString('username'); username ??= prefs.getString('user_name'); if (username == null || username.trim().isEmpty) { final first = prefs.getString('firstname') ?? ''; final last = prefs.getString('lastname') ?? ''; final combined = ('$first $last').trim(); if (combined.isNotEmpty) { username = combined; } } // ✅ Check if there are active pickup to set status final bool hasActivePickups = prefs.getBool('has_live_pickup') ?? false; final String riderStatus = hasActivePickups ? 'active' : 'idle'; final now = DateTime.now(); final iso = _formatDateTimeFull(now); final loginTime = _formatTime(now); final loc = await _ensureLatLng(); final int? tenantid = prefs.getInt('tenantid'); final int? locationid = prefs.getInt('locationid'); final int? applocationid = prefs.getInt('applocationid'); final String? userfcmtoken = prefs.getString('userfcmtoken'); final int? logid = prefs.getInt('logId') ?? prefs.getInt('logid'); final String orderId = prefs.getString('current_riding_order_id') ?? ''; // ── The handset's own state goes IN the heartbeat ── // // It used to be gathered further down, after this post had already gone, // and published to MQTT alone — so the console reading `/miler/logs` had // a position and nothing else, and drew an em dash against Battery, // Connection, GPS Accuracy and Location Service on a rider whose phone // was reporting all four. // // Read here, once, and spread into the payload below. The MQTT publish // further down reuses the same values rather than asking again, so the // two lanes cannot disagree about the same second. See [DeviceTelemetry]. final telemetry = await DeviceTelemetry.read( // The fix lookup already resolved this to get a position at all; // asking a second time can disagree with itself if the rider toggles // the setting in between. locationService: loc['status'], ); final payload = { "logid": logid ?? 0, "userid": userid, "partnerid": partnerid, "shiftid": shiftid, "logdate": iso, "login": loginTime, "latitude": loc['lat'] ?? '0', "longitude": loc['lng'] ?? '0', "raw_latitude": loc['raw_lat'] ?? '0', "raw_longitude": loc['raw_lng'] ?? '0', "velocity_lat": loc['velocity_lat'] ?? '0', "velocity_lng": loc['velocity_lng'] ?? '0', "speed": loc['speed'] ?? '0', "heading": loc['heading'] ?? '0', "onduty": 1, "status": riderStatus, "contactno": prefs.getString('contactno') ?? '', "tenantid": tenantid ?? 0, "locationid": locationid ?? 0, "applocationid": applocationid ?? 0, "userfcmtoken": userfcmtoken ?? '', "username": (username ?? '').trim(), "orderid": orderId, // What the dispatcher needs when a rider goes quiet: the battery, the // signal, whether location is even on, and how good the last fix was. ...telemetry.toPayload(), // Only a measured one — a `0` reads on the console as a perfect fix. if ((loc['accuracy'] ?? '0') != '0') "accuracy": loc['accuracy'], // This heartbeat is the *background* one — it is what keeps reporting // while the app is not on screen, which is exactly the state the // console's App State column exists to show. "is_background": true, }; final firstName = prefs.getString('firstname') ?? ''; final lastName = prefs.getString('lastname') ?? ''; if (firstName.trim().isNotEmpty) { payload['firstname'] = firstName.trim(); } if (lastName.trim().isNotEmpty) { payload['lastname'] = lastName.trim(); } final base = ApiConstants.mainRoute == 'live' ? ApiConstants.createRiderLogLive : ApiConstants.createRiderLogDev; final provider = CreateRiderLogProvider(); final resp = await provider.createRiderLog(payload); if (resp == null || resp.isEmpty) return; final det = (resp['details'] is Map) ? (resp['details'] as Map) : resp; final newLogId = int.tryParse('${det['logid'] ?? 0}') ?? (det['logid'] as int? ?? 0); await prefs.setInt('logid', newLogId); // ✅ MQTT BACKGROUND PUBLISH ( Lane Split ) final mqttService = MilerMqttService(); if (!mqttService.isConnected) { // Use a slightly different client ID for background to avoid kicking the main one off await mqttService.connect(); } if (mqttService.isConnected) { // The same reading the heartbeat above carried — not a second one. // Two reads a second apart can report two batteries, and the console // then shows one number on the live tile and a different one in the // trail it is drawn from. final int batteryLevel = telemetry.battery ?? 0; final bool isCharging = telemetry.isCharging ?? false; final String connType = telemetry.connection ?? 'none'; // 1. Direct Telemetry (Feeding the /full API) mqttService.publish('battery', '$batteryLevel%'); mqttService.publish('charging', isCharging ? 'yes' : 'no'); mqttService.publish('speed', loc['speed'] ?? '0'); mqttService.publish('connection', connType); mqttService.publish('accuracy', loc['accuracy'] ?? '0'); // 2. Alert if Location is Off final String locStatus = loc['status'] ?? 'unknown'; if (locStatus != 'enabled') { mqttService.publish('alerts', { 'userid': userid, 'username': (username ?? '').trim(), 'event': 'location_turned_off', 'error_type': locStatus, 'battery': '$batteryLevel%', 'is_charging': isCharging, 'connection': connType, 'logdate': iso, }); } // 3. Low Battery Alert if (batteryLevel < 15 && !isCharging) { mqttService.publish('alerts', { 'userid': userid, 'username': (username ?? '').trim(), 'event': 'low_battery_warning', 'battery': '$batteryLevel%', 'logdate': iso, }); } // 4. Poor GPS Accuracy Alert final double accuracy = double.tryParse(loc['accuracy'] ?? '0') ?? 0; if (accuracy > 30) { mqttService.publish('alerts', { 'userid': userid, 'username': (username ?? '').trim(), 'event': 'poor_gps_signal', 'accuracy': '${accuracy.toStringAsFixed(1)}m', 'logdate': iso, }); } // 5. Lane: Status mqttService.updateStatus( riderStatus == 'active' ? 'Active' : MqttConstants.statusOnline, ); // 6. Lane: Periodic Log (Comprehensive Snapshot) mqttService.publishLog('rider_periodic_log', { 'userid': userid, 'username': username, 'logdate': iso, 'latitude': loc['lat'] ?? '0', 'longitude': loc['lng'] ?? '0', 'speed': loc['speed'] ?? '0', 'heading': loc['heading'] ?? '0', 'accuracy': loc['accuracy'] ?? '0', 'status': riderStatus, 'orderid': orderId, 'battery': '$batteryLevel%', 'is_charging': isCharging, 'connection': connType, 'location_service': locStatus, 'is_background': true, }); } } catch (e) { // ignore background errors } } } class RiderLogTaskHandler extends TaskHandler { Timer? _timer; // not used; plugin provides repeat callback, but keep safety @override Future onStart(DateTime timestamp, SendPort? sendPort) async { // No-op } @override Future onRepeatEvent(DateTime timestamp, SendPort? sendPort) async { // 1. Rider Log (existing) await _BackgroundRiderLog.createLoginNow(); // 2. Pickup Log (new) await BackgroundCollectionLog.processActivePickups(); // 3. Auto Shift End (new) await BackgroundCollectionLog.checkShiftEnd(); } @override Future onDestroy(DateTime timestamp, SendPort? sendPort) async { _timer?.cancel(); _timer = null; } } @pragma('vm:entry-point') void riderLogCallback() { HttpOverrides.global = MyHttpOverrides(); FlutterForegroundTask.setTaskHandler(RiderLogTaskHandler()); }