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: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/utils/kalman_filter.dart'; import 'package:miler/utils/mqtt_service.dart'; import 'package:miler/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:miler/helpers/http_overrides.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') ?? ''; 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(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) { // 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. 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()); }