feat: add device authentication, place gates support, and PiP navigation features

This commit is contained in:
Hamza-Ayed
2026-09-19 12:34:26 +03:00
parent 279c9fa510
commit a8470eb76c
15 changed files with 652 additions and 37 deletions
@@ -12,6 +12,7 @@ class ApiConstants {
static const String mapSaasSearch = 'https://map-saas.intaleqapp.com/api/geocoding/search';
static const String mapSaasPlaces = 'https://map-saas.intaleqapp.com/api/geocoding/places';
static const String mapSaasTelemetry = 'https://map-saas.intaleqapp.com/api/telemetry';
static const String mapSaasTelemetryBatch = 'https://map-saas.intaleqapp.com/api/telemetry/batch';
static const String mapSaasStyleBase = 'https://map-saas.intaleqapp.com/api/maps/style.json';
static const String mapSaasDeviceProvision = 'https://map-saas.intaleqapp.com/api/auth/device-provision';
static const String googlePlacesNearby = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json';
@@ -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;
}
}