Files
Xpress-rider/lib/background/foreground_service.dart

462 lines
18 KiB
Dart

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<Map<String, String>> _ensureLatLng() async {
Map<String, String> 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<void> _accumulateBackgroundKms(
SharedPreferences prefs,
Map<String, String> 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<void> 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<String, dynamic>)
? (resp['details'] as Map<String, dynamic>)
: 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<void> onStart(DateTime timestamp, SendPort? sendPort) async {
// No-op
}
@override
Future<void> 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<void> onDestroy(DateTime timestamp, SendPort? sendPort) async {
_timer?.cancel();
_timer = null;
}
}
@pragma('vm:entry-point')
void riderLogCallback() {
HttpOverrides.global = MyHttpOverrides();
FlutterForegroundTask.setTaskHandler(RiderLogTaskHandler());
}