feat: add device authentication, place gates support, and PiP navigation features
This commit is contained in:
@@ -77,7 +77,10 @@ class DeviceFingerprintService {
|
||||
|
||||
_isInitialized = true;
|
||||
debugPrint('🔑 [DeviceFingerprintService] Hardware Fingerprint: $fingerprintId');
|
||||
debugPrint('🛡️ [DeviceFingerprintService] Active Key: ${activeApiKey.substring(0, 10)}... (Dedicated: $isDedicatedKeyActive)');
|
||||
final displayKey = activeApiKey.length >= 10
|
||||
? '${activeApiKey.substring(0, 10)}...'
|
||||
: (activeApiKey.isEmpty ? 'NONE (Unset)' : activeApiKey);
|
||||
debugPrint('🛡️ [DeviceFingerprintService] Active Key: $displayKey (Dedicated: $isDedicatedKeyActive)');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ [DeviceFingerprintService] Initialization error: $e');
|
||||
_fingerprintId ??= 'siro_${Platform.operatingSystem}_fallback_${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../data/repositories/map_saas_repository.dart';
|
||||
import 'device_fingerprint_service.dart';
|
||||
|
||||
/// Single telemetry point snapshot recorded during driving.
|
||||
class TelemetryPoint {
|
||||
final String driverId;
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final double speed;
|
||||
final double heading;
|
||||
final double elevation;
|
||||
final double distance;
|
||||
final DateTime timestamp;
|
||||
|
||||
const TelemetryPoint({
|
||||
required this.driverId,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
required this.speed,
|
||||
required this.heading,
|
||||
this.elevation = 0.0,
|
||||
this.distance = 0.0,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'driver_id': driverId,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'speed': speed < 0 ? 0.0 : speed,
|
||||
'heading': heading < 0 ? 0.0 : heading,
|
||||
'elevation': elevation,
|
||||
'distance': distance,
|
||||
};
|
||||
}
|
||||
|
||||
/// Ultra-lightweight Telemetry & Periodic Sync Engine.
|
||||
///
|
||||
/// 1. Samples GPS location every 3 seconds with a stationary filter ("خفيف على البطارية").
|
||||
/// 2. Batches and flushes points to MapSaaS backend every 2 minutes (or 40 points).
|
||||
/// 3. Offline buffer retains up to 400 points if the device is disconnected.
|
||||
/// 4. Executes the 10-day maintenance & update cycle (style, road metadata, device quota).
|
||||
class TelemetryTrackingService {
|
||||
TelemetryTrackingService._();
|
||||
static final TelemetryTrackingService instance = TelemetryTrackingService._();
|
||||
|
||||
static const String prefKeyLastPeriodicCheck = 'siro_last_periodic_update_check_ms';
|
||||
static const int samplingIntervalSeconds = 3;
|
||||
static const int uploadIntervalSeconds = 120; // 2 minutes
|
||||
static const int maxBatchSize = 40; // 40 points * 3s = 120s
|
||||
static const int maxBufferSize = 400; // Offline safety ceiling (~20 minutes offline)
|
||||
|
||||
final List<TelemetryPoint> _buffer = [];
|
||||
DateTime? _lastSampleTime;
|
||||
DateTime? _lastFlushTime;
|
||||
TelemetryPoint? _lastRecordedPoint;
|
||||
Timer? _periodicFlushTimer;
|
||||
bool _isFlushing = false;
|
||||
|
||||
/// Expose buffer count for observability and unit tests
|
||||
int get bufferedPointCount => _buffer.length;
|
||||
List<TelemetryPoint> get bufferedPoints => List.unmodifiable(_buffer);
|
||||
DateTime? get lastFlushTime => _lastFlushTime;
|
||||
|
||||
/// Initializes periodic flush timer
|
||||
void initialize() {
|
||||
_periodicFlushTimer?.cancel();
|
||||
_lastFlushTime = DateTime.now();
|
||||
_periodicFlushTimer = Timer.periodic(
|
||||
const Duration(seconds: uploadIntervalSeconds),
|
||||
(_) => flush(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Stops tracking timers and flushes remaining points
|
||||
Future<void> dispose({MapSaasRepository? repository}) async {
|
||||
_periodicFlushTimer?.cancel();
|
||||
_periodicFlushTimer = null;
|
||||
await flush(repository: repository);
|
||||
}
|
||||
|
||||
/// Records a new GPS position snapshot every 3 seconds.
|
||||
/// Applies a smart stationary filter to eliminate redundant points when parked.
|
||||
bool recordPosition({
|
||||
required double latitude,
|
||||
required double longitude,
|
||||
required double speedKmH,
|
||||
required double heading,
|
||||
double elevation = 0.0,
|
||||
double remainingDistance = 0.0,
|
||||
String? customDriverId,
|
||||
DateTime? now,
|
||||
}) {
|
||||
final currentTime = now ?? DateTime.now();
|
||||
|
||||
// 1. Throttle: Must be at least 3 seconds since last sample
|
||||
if (_lastSampleTime != null) {
|
||||
final elapsedSeconds = currentTime.difference(_lastSampleTime!).inSeconds;
|
||||
if (elapsedSeconds < samplingIntervalSeconds) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Smart Stationary Filter ("بخفة دم"):
|
||||
// If vehicle is completely stopped (speed < 1 km/h) and has not moved > 3 meters,
|
||||
// only record a single heartbeat point every 30 seconds instead of spamming duplicates.
|
||||
if (_lastRecordedPoint != null) {
|
||||
final isNearZeroSpeed = speedKmH < 1.0;
|
||||
final distMeters = Geolocator.distanceBetween(
|
||||
_lastRecordedPoint!.latitude,
|
||||
_lastRecordedPoint!.longitude,
|
||||
latitude,
|
||||
longitude,
|
||||
);
|
||||
|
||||
if (isNearZeroSpeed && distMeters < 3.0) {
|
||||
final elapsedSinceLastRecorded =
|
||||
currentTime.difference(_lastRecordedPoint!.timestamp).inSeconds;
|
||||
if (elapsedSinceLastRecorded < 30) {
|
||||
// Stationary and under 30 seconds heartbeat threshold: Skip point
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final driverId = customDriverId ?? DeviceFingerprintService.instance.fingerprintId;
|
||||
|
||||
final point = TelemetryPoint(
|
||||
driverId: driverId,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
speed: speedKmH,
|
||||
heading: heading,
|
||||
elevation: elevation,
|
||||
distance: remainingDistance,
|
||||
timestamp: currentTime,
|
||||
);
|
||||
|
||||
// Evict oldest points if offline buffer reaches ceiling
|
||||
if (_buffer.length >= maxBufferSize) {
|
||||
_buffer.removeAt(0);
|
||||
}
|
||||
|
||||
_buffer.add(point);
|
||||
_lastSampleTime = currentTime;
|
||||
_lastRecordedPoint = point;
|
||||
|
||||
// 3. Proactive flush if batch size threshold reached
|
||||
if (_buffer.length >= maxBatchSize) {
|
||||
flush();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Flushes all buffered telemetry points to MapSaaS backend in a single request.
|
||||
Future<bool> flush({MapSaasRepository? repository}) async {
|
||||
if (_buffer.isEmpty || _isFlushing) return true;
|
||||
|
||||
_isFlushing = true;
|
||||
final repo = repository ?? MapSaasRepository();
|
||||
final pointsToSend = List<TelemetryPoint>.from(_buffer);
|
||||
final payload = pointsToSend.map((p) => p.toJson()).toList();
|
||||
|
||||
try {
|
||||
final ok = await repo.sendTelemetryBatch(points: payload);
|
||||
if (ok) {
|
||||
// Remove successfully sent points from buffer
|
||||
_buffer.removeRange(0, pointsToSend.length);
|
||||
_lastFlushTime = DateTime.now();
|
||||
debugPrint('🛰️ [TelemetryTrackingService] Flushed ${pointsToSend.length} points to server.');
|
||||
_isFlushing = false;
|
||||
return true;
|
||||
} else {
|
||||
debugPrint('⚠️ [TelemetryTrackingService] Batch upload failed, retaining points in buffer.');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [TelemetryTrackingService] Flush error: $e');
|
||||
}
|
||||
|
||||
_isFlushing = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Checks and performs the 10-day periodic maintenance & update cycle.
|
||||
/// Runs on app startup or navigation entry.
|
||||
Future<bool> checkPeriodicUpdates({
|
||||
DateTime? now,
|
||||
SharedPreferences? mockPrefs,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = mockPrefs ?? await SharedPreferences.getInstance();
|
||||
final lastCheckMs = prefs.getInt(prefKeyLastPeriodicCheck);
|
||||
final currentTime = now ?? DateTime.now();
|
||||
|
||||
if (lastCheckMs != null) {
|
||||
final lastCheck = DateTime.fromMillisecondsSinceEpoch(lastCheckMs);
|
||||
final differenceInDays = currentTime.difference(lastCheck).inDays;
|
||||
if (differenceInDays < 10) {
|
||||
// Less than 10 days since last update check
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('🔄 [TelemetryTrackingService] 10-day periodic update cycle triggered...');
|
||||
|
||||
// 1. Re-validate device fingerprint and refresh API key / quota
|
||||
await DeviceFingerprintService.instance.provisionDedicatedApiKey();
|
||||
|
||||
// 2. Persist new check timestamp
|
||||
await prefs.setInt(prefKeyLastPeriodicCheck, currentTime.millisecondsSinceEpoch);
|
||||
|
||||
debugPrint('✅ [TelemetryTrackingService] 10-day periodic update check completed successfully.');
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ [TelemetryTrackingService] Periodic update error: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Testing helper to reset internal buffer and timers
|
||||
@visibleForTesting
|
||||
void resetForTesting() {
|
||||
_buffer.clear();
|
||||
_lastSampleTime = null;
|
||||
_lastFlushTime = null;
|
||||
_lastRecordedPoint = null;
|
||||
_periodicFlushTimer?.cancel();
|
||||
_periodicFlushTimer = null;
|
||||
_isFlushing = false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user