import 'dart:async'; 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:nearle/views/helpers/constants/apiconstants.dart'; import 'package:nearle/providers/Riderlog/riderlog_provider.dart'; import 'package:nearle/background/backgroundservice.dart'; import 'package:geolocator/geolocator.dart'; import 'package:nearle/utils/kalman_filter.dart'; import 'package:nearle/utils/mqtt_service.dart'; import 'package:nearle/views/helpers/constants/mqtt_constants.dart'; import 'package:battery_plus/battery_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'dart:io'; import 'package:nearle/helpers/http_overrides.dart'; class _BackgroundRiderLog { static NearleKalmanFilter? _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, ), ); // Reject mocked positions (anti-cheat) if (pos.isMocked) { debugPrint('[BG_RIDER_LOG] Mocked position detected — using cached'); return result; } 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 = NearleKalmanFilter(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"; } /// Accumulates cumulative KMs for active deliveries using the foreground service GPS position. /// Only runs when LiveTrackingService (main isolate) hasn't updated in the last 10 seconds, /// which means the app is backgrounded/screen-off/power-saver and the main isolate is dormant. static Future _accumulateBackgroundKms( SharedPreferences prefs, Map loc, ) async { try { // Check if the main isolate's LiveTrackingService is still actively updating final lastLiveUpdateMs = prefs.getInt('live_tracking_last_update_ms') ?? 0; final nowMs = DateTime.now().millisecondsSinceEpoch; final secondsSinceLiveUpdate = (nowMs - lastLiveUpdateMs) / 1000.0; if (secondsSinceLiveUpdate < 10.0) { // Main isolate is active — let it handle KMs to avoid race conditions debugPrint( '[BG_KM] LiveTrackingService active (${secondsSinceLiveUpdate.toStringAsFixed(1)}s ago) — skipping background accumulation', ); return; } // Skip if GPS accuracy is too poor for reliable KM tracking final double accuracy = double.tryParse(loc['accuracy'] ?? '9999') ?? 9999.0; if (accuracy > 50.0) { debugPrint( '[BG_KM] Low-accuracy position (${accuracy.toStringAsFixed(0)}m) — skipping KM accumulation', ); return; } final double currentLat = double.tryParse(loc['lat'] ?? '0') ?? 0.0; final double currentLng = double.tryParse(loc['lng'] ?? '0') ?? 0.0; if (currentLat == 0.0 || currentLng == 0.0) return; final activeDeliveryIds = prefs.getStringList('active_tracking_delivery_ids') ?? []; for (final dId in activeDeliveryIds) { try { final lastLatStr = prefs.getString('delivery_tracking_${dId}_lastLat') ?? ''; final lastLngStr = prefs.getString('delivery_tracking_${dId}_lastLng') ?? ''; final currentCumKm = double.tryParse( prefs.getString('delivery_tracking_${dId}_cumulativeKm') ?? '0', ) ?? 0.0; if (lastLatStr.isNotEmpty && lastLngStr.isNotEmpty) { final lastLat = double.tryParse(lastLatStr) ?? 0.0; final lastLng = double.tryParse(lastLngStr) ?? 0.0; if (lastLat != 0.0 && lastLng != 0.0) { final distanceMeters = Geolocator.distanceBetween( lastLat, lastLng, currentLat, currentLng, ); // Speed-based jump guard: reject if implied speed > 120 km/h (33.3 m/s). // Uses elapsed time since last recorded position so the threshold scales // correctly whether the background interval is 30s, 60s, or longer. final lastUpdateMs = prefs.getInt('delivery_tracking_${dId}_lastUpdateMs') ?? 0; final nowMs = DateTime.now().millisecondsSinceEpoch; final elapsedSeconds = lastUpdateMs > 0 ? (nowMs - lastUpdateMs) / 1000.0 : 60.0; // conservative default final maxRealisticMeters = elapsedSeconds * 33.3; // 120 km/h ceiling if (distanceMeters > maxRealisticMeters && distanceMeters > 50.0) { // GPS jumped — update anchor without counting phantom distance debugPrint( '[BG_KM] GPS jump for $dId: ${distanceMeters.toStringAsFixed(0)}m ' 'in ${elapsedSeconds.toStringAsFixed(1)}s (max: ${maxRealisticMeters.toStringAsFixed(0)}m) — resetting anchor', ); await prefs.setString('delivery_tracking_${dId}_lastLat', currentLat.toString()); await prefs.setString('delivery_tracking_${dId}_lastLng', currentLng.toString()); await prefs.setInt('delivery_tracking_${dId}_lastUpdateMs', nowMs); } else if (distanceMeters >= 5.0) { final newCumKm = currentCumKm + (distanceMeters / 1000.0); await prefs.setString( 'delivery_tracking_${dId}_cumulativeKm', newCumKm.toStringAsFixed(4), ); await prefs.setString('delivery_tracking_${dId}_lastLat', currentLat.toString()); await prefs.setString('delivery_tracking_${dId}_lastLng', currentLng.toString()); await prefs.setInt('delivery_tracking_${dId}_lastUpdateMs', nowMs); debugPrint( '[BG_KM] +${(distanceMeters / 1000.0).toStringAsFixed(4)} km for $dId ' 'in ${elapsedSeconds.toStringAsFixed(1)}s (total: ${newCumKm.toStringAsFixed(4)} km)', ); } } } else { // No anchor yet — set initial position await prefs.setString('delivery_tracking_${dId}_lastLat', currentLat.toString()); await prefs.setString('delivery_tracking_${dId}_lastLng', currentLng.toString()); await prefs.setInt( 'delivery_tracking_${dId}_lastUpdateMs', DateTime.now().millisecondsSinceEpoch, ); } } catch (_) {} } } catch (e) { debugPrint('[BG_KM] Error in background KM accumulation: $e'); } } static Future createLoginNow() async { try { final prefs = await SharedPreferences.getInstance(); // Reload from disk so we see the latest values written by the main isolate await prefs.reload(); 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 deliveries to set status final bool hasActiveDeliveries = prefs.getBool('has_live_deliveries') ?? false; final String riderStatus = hasActiveDeliveries ? 'active' : 'idle'; final now = DateTime.now(); final iso = _formatDateTimeFull(now); final loginTime = _formatTime(now); final loc = await _ensureLatLng(); // Accumulate KMs in background when LiveTrackingService (main isolate) is not active await _accumulateBackgroundKms(prefs, loc); 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') ?? ''; 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, }; 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(base, 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); await prefs.setInt('logId', newLogId); // ✅ MQTT BACKGROUND PUBLISH ( Lane Split ) final mqttService = NearleMqttService(); if (!mqttService.isConnected) { // Use a slightly different client ID for background to avoid kicking the main one off await mqttService.connect(); } if (mqttService.isConnected) { // Gather Telemetry final battery = Battery(); final int batteryLevel = await battery.batteryLevel; final BatteryState batteryState = await battery.batteryState; final isCharging = batteryState == BatteryState.charging || batteryState == BatteryState.full; final connectivity = await Connectivity().checkConnectivity(); final String connType = connectivity.isNotEmpty ? connectivity.first.toString().split('.').last : '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. Delivery Log (new) await BackgroundDeliveryLog.processActiveDeliveries(); // 3. Auto Shift End (new) await BackgroundDeliveryLog.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()); }