import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:latlong2/latlong.dart'; import 'package:share_plus/share_plus.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 { // ── Á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; // ── Belső állapot ────────────────────────────────────────────────────────── LocationSource? _source; StreamSubscription? _positionSub; Timer? _elapsedTimer; TrackPoint? _lastPoint; DateTime? _sessionStart; double _accumulatedDistance = 0; final _db = TrackDatabase.instance; final _exporter = GpxExporter(); // ── 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}) 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 name = DateFormat('yyyy-MM-dd HH:mm').format(now); final trackId = await _db.insertTrack(Track( name: name, startTime: now, source: _source!.displayName, )); 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: name, callback: startTrackingCallback, ); // GPS stream feliratkozás _positionSub = _source!.positionStream.listen( _onPosition, onError: (e) { Get.snackbar('GPS hiba', e.toString(), backgroundColor: Colors.red, colorText: Colors.white); stopRecording(); }, ); // Időmérő _startElapsedTimer(); isRecording.value = true; isPaused.value = false; } 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(), ); await _db.updateTrack(finished); currentTrack.value = finished; } await FlutterForegroundTask.stopService(); await _source?.dispose(); _source = null; isRecording.value = false; isPaused.value = false; await loadSavedTracks(); } Future loadSavedTracks() async { savedTracks.value = await _db.listTracks(); } Future deleteTrack(int id) async { await _db.deleteTrack(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}'); } 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 { if (isPaused.value) return; final trackId = currentTrack.value?.id; if (trackId == null) return; // 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) return; } _accumulatedDistance += segmentDist; sessionDistance.value = _accumulatedDistance; // Sebesség km/h-ban currentSpeedKmh.value = (pos.speed ?? 0) * 3.6; // Pont mentése 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, ); await _db.addPoint(point, _accumulatedDistance); _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, ); } } 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(); } }