import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import 'package:permission_handler/permission_handler.dart'; final locationServiceProvider = Provider((ref) { final service = LocationService(); ref.onDispose(() => service.dispose()); return service; }); final locationStreamProvider = StreamProvider((ref) { final service = ref.watch(locationServiceProvider); return service.getFilteredPositionStream(); }); final locationPermissionGrantedProvider = FutureProvider((ref) async { final service = ref.watch(locationServiceProvider); return await service.hasPermissions(); }); class LocationService { Position? _lastValidPosition; double? _emaSpeed; final StreamController _controller = StreamController.broadcast(); StreamSubscription? _rawPositionSub; Timer? _watchdogTimer; bool _isDisposed = false; // Filtering thresholds static const double _maxRealisticSpeed = 100.0; // roughly 223 MPH in m/s static const double _maxAcceleration = 30.0; // m/s^2 (allows extreme superbike launch / braking while rejecting GPS teleportation) void Function(Position position, bool accepted, String? rejectionReason)? onGpsDiagnostic; LocationService() { _initStream(); } Future requestPermissions() async { try { // 1. Geolocator native permission check & request LocationPermission geoPermission = await Geolocator.checkPermission(); if (geoPermission == LocationPermission.denied) { geoPermission = await Geolocator.requestPermission(); } // 2. Permission handler check & request final locStatus = await Permission.locationWhenInUse.request(); await Permission.notification.request(); if (geoPermission == LocationPermission.whileInUse || geoPermission == LocationPermission.always || locStatus.isGranted) { try { await Permission.locationAlways.request(); } catch (_) {} // Restart/refresh GPS stream now that permissions are granted _startListening(); return true; } } catch (_) {} return false; } Future hasPermissions() async { try { final geoPermission = await Geolocator.checkPermission(); if (geoPermission == LocationPermission.whileInUse || geoPermission == LocationPermission.always) { return true; } final status = await Permission.locationWhenInUse.status; return status.isGranted; } catch (_) { return false; } } Stream getFilteredPositionStream() { return _controller.stream; } void _initStream() { _startListening(); _startWatchdog(); } Future _startListening() async { if (_isDisposed) return; await _rawPositionSub?.cancel(); _rawPositionSub = null; try { final isServiceEnabled = await Geolocator.isLocationServiceEnabled(); if (!isServiceEnabled) { return; } // Immediately bootstrap with last known position if available final lastPos = await Geolocator.getLastKnownPosition(); if (lastPos != null && !_isDisposed) { _handleRawPosition(lastPos); } // Proactively trigger immediate high-accuracy position to warm up GNSS hardware Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.bestForNavigation, timeLimit: Duration(seconds: 4), ), ).then((pos) { if (!_isDisposed) _handleRawPosition(pos); }).catchError((_) {}); // Platform-specific LocationSettings LocationSettings locationSettings; if (defaultTargetPlatform == TargetPlatform.android) { locationSettings = AndroidSettings( accuracy: LocationAccuracy.bestForNavigation, distanceFilter: 0, intervalDuration: const Duration(milliseconds: 100), forceLocationManager: false, // Use Google Fused Location Provider for best accuracy & responsiveness foregroundNotificationConfig: const ForegroundNotificationConfig( notificationText: 'MotorSignal is recording ride telemetry', notificationTitle: 'MotorSignal Active Ride', enableWakeLock: true, ), ); } else { locationSettings = const LocationSettings( accuracy: LocationAccuracy.bestForNavigation, distanceFilter: 0, ); } _rawPositionSub = Geolocator.getPositionStream(locationSettings: locationSettings).listen( (pos) { _handleRawPosition(pos); }, onError: (err) { onGpsDiagnostic?.call( Position( longitude: _lastValidPosition?.longitude ?? 0.0, latitude: _lastValidPosition?.latitude ?? 0.0, timestamp: DateTime.now(), accuracy: 999.0, altitude: 0.0, heading: 0.0, speed: 0.0, speedAccuracy: 0.0, altitudeAccuracy: 0.0, headingAccuracy: 0.0, ), false, 'GPS Stream Error: $err', ); // Try to reconnect in 2 seconds Future.delayed(const Duration(seconds: 2), () { if (!_isDisposed) _startListening(); }); }, cancelOnError: false, ); } catch (_) {} } void _startWatchdog() { _watchdogTimer?.cancel(); // Watchdog every 1000ms: if stream has gone quiet (>2s since last fix), poll getCurrentPosition actively _watchdogTimer = Timer.periodic(const Duration(milliseconds: 1000), (_) async { if (_isDisposed) return; final now = DateTime.now(); final lastFixAge = _lastValidPosition == null ? 999 : now.difference(_lastValidPosition!.timestamp).inMilliseconds; if (lastFixAge > 2000) { try { final hasPerm = await hasPermissions(); if (!hasPerm) return; Position? pos; try { pos = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.bestForNavigation, timeLimit: Duration(seconds: 2), ), ); } catch (_) {} if (pos != null && !_isDisposed) { _handleRawPosition(pos); } } catch (_) {} } }); } String? _lastDiagnosticReason; DateTime? _lastDiagnosticTime; void _emitGpsDiagnostic(Position position, bool accepted, String? reason) { if (!accepted && reason != null) { final now = DateTime.now(); if (_lastDiagnosticReason == reason && _lastDiagnosticTime != null && now.difference(_lastDiagnosticTime!).inMilliseconds < 500) { return; // Debounce duplicate diagnostic message within 500ms } _lastDiagnosticReason = reason; _lastDiagnosticTime = now; } onGpsDiagnostic?.call(position, accepted, reason); } void _handleRawPosition(Position position) { if (_isValidPosition(position)) { final filtered = _applyEmaFilter(position); if (!_controller.isClosed) { _controller.add(filtered); } } } bool _isValidPosition(Position position) { // Allow initial fixes up to 200m so GPS lock begins immediately final currentSpeed = _emaSpeed ?? position.speed; final maxAcceptableAccuracy = _lastValidPosition == null ? 200.0 : (currentSpeed > 10.0 ? 100.0 : 80.0); if (position.accuracy > maxAcceptableAccuracy) { _emitGpsDiagnostic( position, false, 'Accuracy poor (${position.accuracy.toStringAsFixed(1)}m > ${maxAcceptableAccuracy.toStringAsFixed(1)}m)', ); return false; } if (position.speed > _maxRealisticSpeed) { _emitGpsDiagnostic( position, false, 'Impossible speed (${(position.speed * 2.23694).toStringAsFixed(1)} MPH > 223 MPH)', ); return false; } if (_lastValidPosition != null) { final timeDiff = (position.timestamp.difference(_lastValidPosition!.timestamp)).inMilliseconds / 1000.0; // If signal was lost for > 8 seconds, treat as new baseline fix if (timeDiff > 8.0) { _lastValidPosition = position; _emaSpeed = position.speed; _emitGpsDiagnostic(position, true, 'Baseline reset after ${timeDiff.toStringAsFixed(1)}s gap'); return true; } if (timeDiff >= 0.2) { // Acceleration check final accel = (position.speed - _lastValidPosition!.speed).abs() / timeDiff; if (accel > _maxAcceleration) { _emitGpsDiagnostic( position, false, 'Impossible acceleration (${accel.toStringAsFixed(1)} m/s² > $_maxAcceleration m/s²)', ); return false; } // Distance teleportation / derived velocity check final distMeters = Geolocator.distanceBetween( _lastValidPosition!.latitude, _lastValidPosition!.longitude, position.latitude, position.longitude, ); final derivedSpeedMps = distMeters / timeDiff; if (derivedSpeedMps > _maxRealisticSpeed && distMeters > 50.0) { _emitGpsDiagnostic( position, false, 'GPS position teleportation (${distMeters.toStringAsFixed(0)}m in ${timeDiff.toStringAsFixed(1)}s)', ); return false; } } } _lastValidPosition = position; _emitGpsDiagnostic(position, true, null); return true; } Position _applyEmaFilter(Position position) { final rawSpeed = position.speed; // Stationary jitter deadband: Speeds under 0.6 m/s (~1.3 MPH) are treated as stationary 0.0 MPH if (rawSpeed < 0.6) { _emaSpeed = 0.0; } else { if (_emaSpeed == null || _emaSpeed == 0.0) { _emaSpeed = rawSpeed; } else { // Fast dynamic alpha: 0.90 when accelerating/braking so speedometer tracks instantly, 0.75 when cruising final delta = (rawSpeed - _emaSpeed!).abs(); final alpha = delta > 0.4 ? 0.90 : 0.75; _emaSpeed = alpha * rawSpeed + (1 - alpha) * _emaSpeed!; } } return Position( longitude: position.longitude, latitude: position.latitude, timestamp: position.timestamp, accuracy: position.accuracy, altitude: position.altitude, heading: position.heading, speed: _emaSpeed ?? 0.0, speedAccuracy: position.speedAccuracy, altitudeAccuracy: position.altitudeAccuracy, headingAccuracy: position.headingAccuracy, isMocked: position.isMocked, floor: position.floor, ); } void dispose() { _isDisposed = true; _watchdogTimer?.cancel(); _rawPositionSub?.cancel(); _controller.close(); } }