import 'dart:async'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:latlong2/latlong.dart'; import 'package:share_plus/share_plus.dart'; import 'package:terepi_seged/services/app_logger.dart'; import 'package:terepi_seged/services/firebase_logger.dart'; import 'package:terepi_seged/services/project_service.dart'; import 'package:terepi_seged/services/track_sync_service.dart'; import '../../../../services/location_source.dart'; import '../../../../services/phone_gps_source.dart'; import '../../../../services/track_database.dart'; import '../../../../services/gpx_exporter.dart'; import '../../../../models/track.dart'; // ─── Foreground task handler ───────────────────────────────────────────────── // Ez a függvény a háttér-isolate-ban fut. Csak a meglevő pozíció-streamet // tartja fenn, a tényleges adatfeldolgozás a főszálban történik. @pragma('vm:entry-point') void startTrackingCallback() { FlutterForegroundTask.setTaskHandler(_TrackingTaskHandler()); } class _TrackingTaskHandler extends TaskHandler { @override Future onStart(DateTime timestamp, TaskStarter starter) async {} @override void onRepeatEvent(DateTime timestamp) { // A Geolocator stream a főizoláton folyik tovább, itt nincs teendő. } @override Future onDestroy(DateTime timestamp, bool isTimeout) async {} } // ─── TrackingController ────────────────────────────────────────────────────── class TrackingController extends GetxController { static TrackingController get to => Get.find(); // ── Állapot ──────────────────────────────────────────────────────────────── final isRecording = false.obs; final isPaused = false.obs; final currentTrack = Rxn(); /// Aktuális session pontjai a térképhez (csak a memóriában). final livePoints = [].obs; /// Aktuális sebesség [km/h]. final currentSpeedKmh = 0.0.obs; /// Megtett távolság [m] az aktuális sessionben. final sessionDistance = 0.0.obs; /// Eltelt idő formátuma óó:pp:mm. final elapsedFormatted = '00:00:00'.obs; /// Előtér/háttér állapot jelzése. final isInBackground = false.obs; // ── Mentett track-ek listája ──────────────────────────────────────────────── final savedTracks = [].obs; // Melyik track-ek látszanak overlay-ként a térképen final overlayTrackIds = [].obs; // Betöltött koordináták cache final Map> _trackCoords = {}; int get livePointCount => livePoints.length; // ── Belső állapot ────────────────────────────────────────────────────────── LocationSource? _source; StreamSubscription? _positionSub; Timer? _elapsedTimer; TrackPoint? _lastPoint; DateTime? _sessionStart; double _accumulatedDistance = 0; final _db = TrackDatabase.instance; final _exporter = GpxExporter(); // Timer DateTime? _lastPositionTime; Timer? _watchdogTimer; final gpsSignalLost = false.obs; static const _gpsTimeoutSec = 10; // ── Inicializálás ────────────────────────────────────────────────────────── @override void onInit() { super.onInit(); _initForegroundTask(); loadSavedTracks(); } void _initForegroundTask() { FlutterForegroundTask.init( androidNotificationOptions: AndroidNotificationOptions( channelId: 'tracking_channel', channelName: 'Nyomvonal rögzítés', channelDescription: 'Aktív track rögzítése folyamatban', channelImportance: NotificationChannelImportance.LOW, priority: NotificationPriority.LOW, ), iosNotificationOptions: const IOSNotificationOptions( showNotification: true, playSound: false, ), foregroundTaskOptions: ForegroundTaskOptions( eventAction: ForegroundTaskEventAction.repeat(1000), autoRunOnBoot: false, allowWakeLock: true, allowWifiLock: false, ), ); } // ── Publikus API ─────────────────────────────────────────────────────────── /// Elindítja a rögzítést a megadott forrással (alapértelmezett: telefon GPS). Future startRecording( {LocationSource? source, String? name, TrackSyncMode syncMode = TrackSyncMode.online}) async { if (isRecording.value) return; _source = source ?? PhoneGpsSource(intervalMs: 1000, distanceFilter: 2.0); // Track létrehozása az adatbázisban final now = DateTime.now(); final trackName = name ?? DateFormat('yyyy-MM-dd HH:mm').format(now); final trackId = await _db.insertTrack(Track( name: trackName, startTime: now, source: _source!.displayName, projectId: ProjectService.to.activeProjectId, syncMode: syncMode)); currentTrack.value = await _db.getTrack(trackId); // Állapot reset livePoints.clear(); sessionDistance.value = 0; _accumulatedDistance = 0; _lastPoint = null; _sessionStart = now; // Foreground service indítása (háttér-működéshez) await FlutterForegroundTask.startService( notificationTitle: 'Track rögzítése', notificationText: trackName, callback: startTrackingCallback, ); if (!(currentTrack.value?.isLocalOnly ?? true)) { TrackSyncService.to .createRemoteTrack(currentTrack.value!) .then((supabaseId) async { if (supabaseId == null) return; final updated = currentTrack.value!.copyWith(supabaseId: supabaseId); await _db.updateTrack(updated); currentTrack.value = updated; TrackSyncService.to.startSession(updated); }); } else { TrackSyncService.to.startSession(currentTrack.value!); } // GPS stream feliratkozás _positionSub = _source!.positionStream.listen( _onPosition, onError: (e) { AppLogger.e('positionStream', 'Stream hiba', error: e); FirebaseLogger.e('positionStream', 'Stream hiba', error: e); if (e is LocationServiceDisabledException || e is PermissionDeniedException) { Get.snackbar('GPS hiba', e.toString(), backgroundColor: Colors.red, colorText: Colors.white); stopRecording(); } }, ); AppLogger.e('track_started', 'name:$trackName'); FirebaseLogger.event('track_started', {'name': trackName}); // Időmérő _startElapsedTimer(); isRecording.value = true; isPaused.value = false; _startWatchdog(); } void pauseRecording() { if (!isRecording.value || isPaused.value) return; _positionSub?.pause(); _elapsedTimer?.cancel(); isPaused.value = true; FlutterForegroundTask.updateService( notificationTitle: 'Track szüneteltetve', notificationText: currentTrack.value?.name ?? '', ); } void resumeRecording() { if (!isPaused.value) return; _positionSub?.resume(); _startElapsedTimer(); isPaused.value = false; FlutterForegroundTask.updateService( notificationTitle: 'Track rögzítése', notificationText: currentTrack.value?.name ?? '', ); } Future stopRecording() async { if (!isRecording.value) return; await _positionSub?.cancel(); _positionSub = null; _elapsedTimer?.cancel(); _elapsedTimer = null; // Track lezárása az adatbázisban final track = currentTrack.value; if (track?.id != null) { final finished = track!.copyWith( status: TrackStatus.finished, endTime: DateTime.now(), distanceMeters: _accumulatedDistance, pointCount: livePoints.length, ); await _db.updateTrack(finished); await TrackSyncService.to.stopSession(finished); currentTrack.value = finished; } await FlutterForegroundTask.stopService(); await _source?.dispose(); _source = null; isRecording.value = false; isPaused.value = false; await loadSavedTracks(); _watchdogTimer?.cancel(); _watchdogTimer = null; gpsSignalLost.value = false; } Future loadSavedTracks() async { savedTracks.value = await _db.listTracks(); } Future deleteTrack(int id) async { await _db.deleteTrack(id); overlayTrackIds.remove(id); _trackCoords.remove(id); await loadSavedTracks(); } /// GPX export + rendszer megosztás dialóg. Future exportTrack(Track track) async { try { final path = await _exporter.export(track); await Share.shareXFiles([XFile(path)], subject: 'Nyomvonal: ${track.name}.gpx'); } catch (e) { Get.snackbar('Export hiba', e.toString(), backgroundColor: Colors.red, colorText: Colors.white); } } /// Visszatölt egy mentett track pontjait a térképre (megtekintés). Future> loadTrackPoints(int trackId) async { final pts = await _db.getLatLons(trackId); return pts.map((p) => LatLng(p.lat, p.lon)).toList(); } // ── Belső logika ─────────────────────────────────────────────────────────── Future _onPosition(SourcePosition pos) async { try { if (isPaused.value) return; _lastPositionTime = DateTime.now(); gpsSignalLost.value = false; final sw = Stopwatch()..start(); final trackId = currentTrack.value?.id; if (trackId == null) return; final point = TrackPoint( trackId: trackId, latitude: pos.latitude, longitude: pos.longitude, altitude: pos.altitude, accuracy: pos.accuracy, speed: pos.speed, heading: pos.heading, timestamp: pos.timestamp, ); // Távolság a legutóbbi ponttól double segmentDist = 0; if (_lastPoint != null) { segmentDist = haversineMeters( _lastPoint!.latitude, _lastPoint!.longitude, pos.latitude, pos.longitude, ); // Szűrés: ugrásszerű változás (pl. GPS lock elvesztése) ignorálása if (segmentDist > 100) { AppLogger.w( '_onPosition', 'GPS ugrás kiszűrve: ${segmentDist.toStringAsFixed(0)}m ' '(pts: ${livePoints.length})'); _lastPoint = point; // reset — következő pont ettől mér return; } } _accumulatedDistance += segmentDist; sessionDistance.value = _accumulatedDistance; // Sebesség km/h-ban currentSpeedKmh.value = (pos.speed ?? 0) * 3.6; // Pont mentése await _db.addPoint(point, _accumulatedDistance); sw.stop(); if (sw.elapsedMilliseconds > 300) { AppLogger.w( 'on_Position', 'Lassú addPoint: ${sw.elapsedMilliseconds}ms, ' 'pts: ${livePoints.length}'); FirebaseLogger.w( 'on_Position', 'Lassú addPoint: ${sw.elapsedMilliseconds}ms, ' 'pts: ${livePoints.length}'); } TrackSyncService.to.onNewPoint(point); _lastPoint = point; // UI frissítés livePoints.add(LatLng(pos.latitude, pos.longitude)); // Értesítés frissítése if (livePoints.length % 10 == 0) { final dist = _formatDistance(_accumulatedDistance); FlutterForegroundTask.updateService( notificationTitle: 'Track rögzítése – $dist', notificationText: elapsedFormatted.value, ); } } catch (e, stack) { AppLogger.e('_onPosition', 'pont feldolgozási hiba - track folytatódik', error: e, stack: stack); } } void _startElapsedTimer() { _elapsedTimer = Timer.periodic(const Duration(seconds: 1), (_) { if (_sessionStart == null) return; final d = DateTime.now().difference(_sessionStart!); final h = d.inHours.toString().padLeft(2, '0'); final m = (d.inMinutes % 60).toString().padLeft(2, '0'); final s = (d.inSeconds % 60).toString().padLeft(2, '0'); elapsedFormatted.value = '$h:$m:$s'; }); } String _formatDistance(double m) { if (m < 1000) return '${m.toStringAsFixed(0)} m'; return '${(m / 1000).toStringAsFixed(2)} km'; } @override void onClose() { stopRecording(); if (!isRecording.value) { _positionSub?.cancel(); _elapsedTimer?.cancel(); } super.onClose(); } // Overlay ki/be kapcsolása — ha bekapcsol, betölti a koordinátákat void toggleTrackOverlay(int trackId) { if (overlayTrackIds.contains(trackId)) { overlayTrackIds.remove(trackId); overlayTrackIds.refresh(); } else { overlayTrackIds.add(trackId); overlayTrackIds.refresh(); _loadTrackCoords(trackId); } } /// Koordináták visszaadása — üres lista ha még nincs betöltve List getCoordsFor(int trackId) => _trackCoords[trackId] ?? []; Future _loadTrackCoords(int trackId) async { if (_trackCoords.containsKey(trackId)) return; final pts = await _db.getLatLons(trackId); _trackCoords[trackId] = pts.map((p) => LatLng(p.lat, p.lon)).toList(); overlayTrackIds.refresh(); // térkép frissítés } void _startWatchdog() { _watchdogTimer = Timer.periodic( const Duration(seconds: 5), (_) => _checkGpsTimeout(), ); } void _checkGpsTimeout() { if (!isRecording.value || isPaused.value) return; final last = _lastPositionTime; if (last == null) return; final elapsedSec = DateTime.now().difference(last).inSeconds; if (elapsedSec < _gpsTimeoutSec) return; if (!gpsSignalLost.value) { gpsSignalLost.value = true; AppLogger.w( 'watchdog', 'GPS jel elveszett: ${elapsedSec}s óta nincs pozíció ' '(pts: ${livePoints.length}, ' 'táv: ${_formatDistance(_accumulatedDistance)})'); AppLogger.w( 'gps_signal_lost', 'elapsed_sec: ${elapsedSec}' 'point_count: ${livePoints.length}' 'distance_m: ${_accumulatedDistance.round()}'); // Foreground notification frissítése FlutterForegroundTask.updateService( notificationTitle: '⚠ GPS jel elveszett', notificationText: 'Track szünetel — folytatódik ha visszajön a jel', ); // Snackbar ha előtérben van az app if (!isInBackground.value) { Get.snackbar( 'GPS jel elveszett', 'Valószínűleg alagút vagy lefedettség hiány.\n' 'A rögzítés automatikusan folytatódik.', duration: const Duration(seconds: 6), backgroundColor: Colors.orange.shade700, colorText: Colors.white, icon: const Icon(Icons.gps_off, color: Colors.white), snackPosition: SnackPosition.TOP, ); } } // Folyamatosan logol amíg nincs jel (percenként egyszer) if (elapsedSec % 60 == 0) { AppLogger.w('watchdog', 'GPS még mindig hiányzik: ${elapsedSec}s'); } } }