import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'flight_record.dart'; import 'flight_record_sync.dart'; import 'fly_prefs.dart'; /// Where recorded flights live on the phone. abstract class FlightRecordStore { Future> loadAll(); Future save(FlightRecord r); Future delete(String id); } /// One JSON file per flight under the app's documents directory /// (`flight_records/.json`); an unreadable file is skipped, not fatal. class FileFlightRecordStore implements FlightRecordStore { FileFlightRecordStore({Future Function()? root}) : _root = root ?? getApplicationDocumentsDirectory; final Future Function() _root; Directory? _dir; Future _folder() async { if (_dir != null) return _dir!; final Directory d = Directory('${(await _root()).path}${Platform.pathSeparator}flight_records'); if (!await d.exists()) await d.create(recursive: true); return _dir = d; } File _file(Directory d, String id) => File('${d.path}${Platform.pathSeparator}$id.json'); @override Future> loadAll() async { final Directory d = await _folder(); final List out = []; await for (final FileSystemEntity f in d.list()) { if (f is! File || !f.path.endsWith('.json')) continue; try { final Object? j = jsonDecode(await f.readAsString()); if (j is Map) out.add(FlightRecord.fromJson(Map.from(j))); } catch (_) { // A half-written file from a crash: skip it. } } return out; } @override Future save(FlightRecord r) async { final File f = _file(await _folder(), r.id); // Write then rename, so a crash never leaves a truncated record. final File tmp = File('${f.path}.tmp'); await tmp.writeAsString(jsonEncode(r.toJson()), flush: true); await tmp.rename(f.path); } @override Future delete(String id) async { final File f = _file(await _folder(), id); if (await f.exists()) await f.delete(); } } /// The phone's flight records (plan P9): every flight the [FlightRecorder] /// finishes is saved here and, while signed in and /// [FlyPrefs.autoUploadFlights] is on, uploaded to the API Server's logbook. /// Flights that could not be uploaded stay pending and retry on [uploadPending]. class FlightLog extends ChangeNotifier { FlightLog({required this.store, required this.remote}); final FlightRecordStore store; final LogbookRemote remote; List _records = []; bool _loaded = false; final Set _uploading = {}; /// Newest first. List get records => List.unmodifiable(_records); bool get loaded => _loaded; bool isUploading(FlightRecord r) => _uploading.contains(r.id); /// Real flights not in the logbook yet. List get pending => [for (final FlightRecord r in _records) if (!r.uploaded && !r.simulated) r]; /// The latest flight, which is where Find My Drone looks first. FlightRecord? get latest => _records.isEmpty ? null : _records.first; Future load() async { try { _records = await store.loadAll(); } catch (_) { _records = []; } _sort(); _loaded = true; notifyListeners(); } void _sort() => _records.sort((FlightRecord a, FlightRecord b) => b.start.compareTo(a.start)); /// A flight the recorder just finished. Future add(FlightRecord r) async { _records.removeWhere((FlightRecord x) => x.id == r.id); _records.add(r); _sort(); notifyListeners(); await _save(r); if (FlyPrefs.autoUploadFlights.value && remote.available && !r.simulated) await upload(r); } Future _save(FlightRecord r) async { try { await store.save(r); } catch (_) { // Storage full or gone: the record lives in memory until the app closes. } } Future upload(FlightRecord r) async { if (r.uploaded || !_uploading.add(r.id)) return r; notifyListeners(); try { await uploadFlightRecord(r, remote); await _save(r); } finally { _uploading.remove(r.id); notifyListeners(); } return r; } /// Retries every pending flight; answers how many made it. Future uploadPending() async { if (!remote.available) return 0; int ok = 0; for (final FlightRecord r in pending) { if ((await upload(r)).uploaded) ok++; } return ok; } Future delete(FlightRecord r) async { _records.removeWhere((FlightRecord x) => x.id == r.id); notifyListeners(); try { await store.delete(r.id); } catch (_) {} } }