142 lines
6.0 KiB
Dart
142 lines
6.0 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:archive/archive.dart';
|
|
import 'package:drift/drift.dart' as drift;
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import '../../core/version/app_identity.dart';
|
|
import '../../database/database.dart';
|
|
import '../export_service.dart';
|
|
import '../field_test/field_test_export_service.dart';
|
|
|
|
/// Top-level isolate entrypoint for CPU-heavy ZIP compression.
|
|
/// Never receives non-isolate-safe objects (database handles, streams, etc.).
|
|
Uint8List compressFilesToZipIsolate(Map<String, String> filesMap) {
|
|
final archive = Archive();
|
|
for (final entry in filesMap.entries) {
|
|
final bytes = utf8.encode(entry.value);
|
|
archive.addFile(ArchiveFile(entry.key, bytes.length, bytes));
|
|
}
|
|
final zipEncoder = ZipEncoder();
|
|
final compressed = zipEncoder.encode(archive);
|
|
return Uint8List.fromList(compressed);
|
|
}
|
|
|
|
/// Service that queries local forensic datasets and offloads ZIP compression
|
|
/// to a background worker isolate via compute(), preventing main thread stutter.
|
|
class SyncBundleWorker {
|
|
final AppDatabase _db;
|
|
|
|
SyncBundleWorker(this._db);
|
|
|
|
/// Generates the standard multi-format forensic bundle for a ride session
|
|
/// and compresses it inside a background isolate.
|
|
Future<Uint8List?> generateCompressedBundle(int rideId) async {
|
|
try {
|
|
final ride = await (_db.select(_db.rides)..where((r) => r.id.equals(rideId))).getSingleOrNull();
|
|
if (ride == null) return null;
|
|
|
|
final fieldTestExport = FieldTestExportService(_db);
|
|
final exportService = ExportService(_db);
|
|
|
|
// 1. Query local authoritative data on the main isolate
|
|
final sessionJson = await fieldTestExport.generateFieldTestSessionJson(rideId);
|
|
final eventsCsv = await fieldTestExport.generateFieldTestEventsCsv(rideId);
|
|
final telemetryCsv = await exportService.exportRideCsv(rideId);
|
|
final gpxContent = await exportService.exportRideGpx(rideId);
|
|
|
|
final samples = await (_db.select(_db.telemetrySamples)
|
|
..where((t) => t.rideId.equals(rideId))
|
|
..orderBy([(t) => drift.OrderingTerm(expression: t.timestamp)])
|
|
).get();
|
|
|
|
final events = await (_db.select(_db.fieldTestEvents)
|
|
..where((e) => e.rideId.equals(rideId))
|
|
..orderBy([(e) => drift.OrderingTerm(expression: e.timestamp)])
|
|
).get();
|
|
|
|
final summaryRecord = await (_db.select(_db.fieldTestSummaries)
|
|
..where((s) => s.rideId.equals(rideId))
|
|
).getSingleOrNull();
|
|
|
|
final appIdentity = AppIdentity.current();
|
|
|
|
// 2. Build README.txt audit header
|
|
final readme = StringBuffer();
|
|
readme.writeln('=====================================================');
|
|
readme.writeln('MOTORCYCLE DASH — FORENSIC FIELD TEST SESSION REPORT');
|
|
readme.writeln('=====================================================');
|
|
readme.writeln('Application Version: ${appIdentity.semanticVersion}');
|
|
readme.writeln('Build Number: ${appIdentity.buildNumber}');
|
|
readme.writeln('Git Revision: ${appIdentity.gitCommit}');
|
|
readme.writeln('Git Branch: ${appIdentity.gitBranch}');
|
|
readme.writeln('Release Channel: ${appIdentity.releaseChannel}');
|
|
readme.writeln('Telemetry Schema Version: ${appIdentity.telemetrySchemaVersion}');
|
|
readme.writeln('Export Format Version: ${appIdentity.exportSchemaVersion}');
|
|
readme.writeln('Field Test Session / Ride ID: $rideId');
|
|
readme.writeln('Session Start: ${ride.startTime.toIso8601String()}');
|
|
readme.writeln('Session End: ${ride.endTime?.toIso8601String() ?? "IN_PROGRESS"}');
|
|
readme.writeln('Summary Rating: ${summaryRecord?.rideQualityRating ?? "COMPLETED"}');
|
|
readme.writeln('Ride Quality Score: ${summaryRecord?.rideQualityScore ?? 100} / 100');
|
|
readme.writeln('Total Telemetry Samples: ${samples.length}');
|
|
readme.writeln('Total Forensic Events: ${events.length}');
|
|
|
|
// 3. Build ride_summary.json
|
|
final rideSummaryJson = const JsonEncoder.withIndent(' ').convert({
|
|
'rideId': ride.id,
|
|
'startTime': ride.startTime.toIso8601String(),
|
|
'endTime': ride.endTime?.toIso8601String(),
|
|
'distanceMiles': ride.distanceMiles,
|
|
'topSpeedMph': ride.topSpeedMph,
|
|
'maxLeanLeft': ride.maxLeanLeft,
|
|
'maxLeanRight': ride.maxLeanRight,
|
|
'maxAccelG': ride.maxAccelG,
|
|
'maxBrakingG': ride.maxBrakingG,
|
|
'averageSpeed': ride.averageSpeed,
|
|
'movingTime': ride.movingTime,
|
|
'stoppedTime': ride.stoppedTime,
|
|
'qualityScore': summaryRecord?.rideQualityScore ?? 100,
|
|
'qualityRating': summaryRecord?.rideQualityRating ?? 'EXCELLENT',
|
|
'motorcycleId': ride.motorcycleId,
|
|
'motorcycleMake': ride.motorcycleMake,
|
|
'motorcycleModel': ride.motorcycleModel,
|
|
'motorcycleYear': ride.motorcycleYear,
|
|
'software': appIdentity.toJson(),
|
|
});
|
|
|
|
// 4. Assemble isolated serializable string DTO map
|
|
final filesMap = <String, String>{
|
|
'README.txt': readme.toString(),
|
|
'ride_summary.json': rideSummaryJson,
|
|
'field_test_session.json': sessionJson,
|
|
'field_test_events.csv': eventsCsv,
|
|
'telemetry.csv': telemetryCsv,
|
|
};
|
|
|
|
if (gpxContent.isNotEmpty) {
|
|
filesMap['telemetry.gpx'] = gpxContent;
|
|
}
|
|
|
|
// 5. Offload CPU-heavy compression to background worker isolate
|
|
return await compute(compressFilesToZipIsolate, filesMap);
|
|
} catch (e) {
|
|
debugPrint('[SyncBundleWorker] Bundle generation error: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Writes compressed bytes to a temporary file for local inspection or upload
|
|
Future<File?> writeBundleToTempFile(int rideId, Uint8List zipBytes) async {
|
|
try {
|
|
final tempDir = await getTemporaryDirectory();
|
|
final dateStr = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
|
|
final file = File('${tempDir.path}/ride_${rideId}_$dateStr.zip');
|
|
return await file.writeAsBytes(zipBytes);
|
|
} catch (e) {
|
|
debugPrint('[SyncBundleWorker] Error writing temp zip file: $e');
|
|
return null;
|
|
}
|
|
}
|
|
}
|