diff --git a/apps/siro_maps/APP_REVIEW_INSTRUCTIONS.md b/apps/siro_maps/APP_REVIEW_INSTRUCTIONS.md
new file mode 100644
index 0000000..26cedff
--- /dev/null
+++ b/apps/siro_maps/APP_REVIEW_INSTRUCTIONS.md
@@ -0,0 +1,80 @@
+# تعليمات فاحصي متجر التطبيقات (App Review & Testing Instructions)
+## لمتجر جوجل بلاي (Google Play Console) ومتجر آبل (App Store Connect)
+
+---
+
+### أولاً: النص المخصص للنسخ المباشر إلى Google Play Console (باللغة الإنجليزية - المعتمدة للمراجعين)
+
+> **مكان اللصق في Google Play Console**:
+> انتقل إلى: **سياسة التطبيق (App content)** ➔ **الوصول إلى التطبيق (App access)** ➔ اختر **"جميع الوظائف أو بعضها خاضع لقيود" (All or some functionality is restricted)** ➔ أضف تعليمات جديدة:
+
+```text
+IMPORTANT NOTICE FOR APP REVIEWERS:
+
+1. Operational Geographic Coverage:
+Uruk Map is engineered on an independent, self-hosted regional spatial vector engine. Our active vector map tiles and routing servers are currently provisioned specifically for three operational countries in the MENA region:
+- Jordan (الأردن)
+- Iraq (العراق)
+- Egypt (مصر)
+
+If you test the app on a physical device or emulator located outside these three countries (e.g., from the United States or Europe) with local GPS enabled, the camera will center on your local coordinates where our regional map tiles are not rendered, resulting in an empty tile canvas. This is expected behavior due to localized regional hosting.
+
+2. How to Test and Review the App Successfully:
+
+OPTION A (Recommended - Instant In-App Search):
+1. Launch the app and grant or dismiss location permissions.
+2. In the top search bar, type any landmark or city within our coverage area:
+ - For Jordan: Type "Amman" or "عمان" or "City Mall"
+ - For Iraq: Type "Baghdad" or "بغداد" or "Tahrir Square"
+ - For Egypt: Type "Cairo" or "القاهرة" or "Nasr City"
+3. Select any result. The map will instantly fly to the location, load rich vector tiles, 3D buildings, and gates.
+4. Tap "Start Navigation" or "ابدأ الملاحة" to experience turn-by-turn routing, Arabic voice guidance, and vehicle markers.
+
+OPTION B (Simulated GPS Location):
+In your emulator or testing device, set a mock GPS location to any of our operational hubs:
+- Amman (Jordan): Latitude 31.9539, Longitude 35.9106
+- Baghdad (Iraq): Latitude 33.3152, Longitude 44.3661
+- Cairo (Egypt): Latitude 30.0444, Longitude 31.2357
+The app will immediately render full street networks and navigation HUD.
+
+3. Authentication & Credentials:
+No username or password is required. The application automatically registers via secure hardware device fingerprinting and provisions a consumer API key seamlessly on initial launch.
+
+For any technical inquiries during review, please contact: support@intaleqapp.com
+```
+
+---
+
+### ثانياً: النص باللغة العربية (للتوثيق الداخلي والمراجعين الناطقين بالعربية)
+
+```text
+ملاحظة هامة لفاحصي ومراجعي التطبيق:
+
+1. نطاق التغطية الجغرافية التشغيلية:
+تعتمد «خرائط أوروك» على منظومة خرائط وملاحة فيكتورية مستقلة ومستضافة ذاتياً، وتغطي خوادم الخرائط والبيانات المكانية الحالية ثلاث دول تشغيلية رئيسية في منطقة الشرق الأوسط وشمال أفريقيا:
+- الأردن
+- العراق
+- مصر
+
+في حال تم فتح التطبيق واختباره من جهاز يقع خارج هذه الدول الثلاث (مثل الولايات المتحدة أو أوروبا) مع تفعيل نظام تحديد المواقع المحلي (GPS)، فإن الكاميرا ستتوجه لموقع الجهاز الحالي حيث لا تتوفر بلاطات خرائط محلية خارج نطاق التغطية، مما يؤدي لعدم ظهور تفاصيل الطرق. هذا سلوك طبيعي ناتج عن التخصيص الجغرافي للمنظومة.
+
+2. كيفية فحص وتجربة التطبيق بنجاح:
+
+الخيار الأول (الموصى به - عبر البحث المباشر داخل التطبيق):
+1. افتح التطبيق، ثم في شريط البحث العلوي ابحث عن أي مدينة أو معلم في الدول المدعومة:
+ - الأردن: ابحث عن "عمان" أو "سيتي مول" أو "الدوار السابع"
+ - العراق: ابحث عن "بغداد" أو "ساحة التحرير" أو "أربيل"
+ - مصر: ابحث عن "القاهرة" أو "ميدان التحرير" أو "مدينة نصر"
+2. اختر أي نتيجة من القائمة، وستنتقل الخريطة فوراً للموقع وتظهر البلاطات الفيكتورية وتفاصيل الطرق والبوابات.
+3. اضغط على زر "ابدأ الملاحة" لتجربة مسارات السير والتوجيه الصوتي ونمط القيادة.
+
+الخيار الثاني (عبر محاكاة الموقع GPS Mock Location):
+في المحاكي أو جهاز الفحص، قم بتعيين إحداثيات موقع وهمي ضمن إحدى المدن التالية:
+- عمان (الأردن): خط العرض 31.9539 ، خط الطول 35.9106
+- بغداد (العراق): خط العرض 33.3152 ، خط الطول 44.3661
+- القاهرة (مصر): خط العرض 30.0444 ، خط الطول 31.2357
+وستعمل الخريطة والملاحة اللحظية على الفور.
+
+3. بيانات تسجيل الدخول والوصول:
+لا يتطلب التطبيق أي اسم مستخدم أو كلمة مرور. التطبيق يعمل فوراً بمجرد الفتح ويقوم بتوليد مفتاح وصول مشفر عبر بصمة الجهاز تلقائياً.
+```
diff --git a/apps/siro_maps/GOOGLE_PLAY_STORE_LISTING.md b/apps/siro_maps/GOOGLE_PLAY_STORE_LISTING.md
index 5cf2858..60798a1 100644
--- a/apps/siro_maps/GOOGLE_PLAY_STORE_LISTING.md
+++ b/apps/siro_maps/GOOGLE_PLAY_STORE_LISTING.md
@@ -183,3 +183,45 @@
- هل يشارك التطبيق الموقع الجغرافي الدقيق للمستخدم مع أطراف خارجية لأغراض إعلانية؟ **لا (No)**
- هل يستخدم التطبيق الموقع لأغراض تقديم خدمات الملاحة والتوجيه للمستخدم؟ **نعم (Yes)**
- **النتيجة التلقائية**: تصنيف **مناسب للجميع (Everyone / PEGI 3)**.
+
+---
+
+## 6. تعليمات الوصول للفاحصين (App Access & Reviewer Instructions)
+
+> **مكان اللصق في Google Play Console**:
+> من القائمة الجانبية: **سياسة التطبيق (App content)** ➔ **الوصول إلى التطبيق (App access)** ➔ اختر **"جميع الوظائف أو بعضها خاضع لقيود" (All or some functionality is restricted)** ➔ أضف الإرشادات التالية باللغة الإنجليزية:
+
+### نص الإرشادات بالإنجليزية (لإدخاله في خانة تعليمات فاحصي جوجل):
+```text
+IMPORTANT NOTICE FOR APP REVIEWERS:
+
+1. Operational Geographic Coverage:
+Uruk Map operates on an independent, self-hosted regional spatial vector engine. Our vector map tiles and routing servers are currently live specifically for three operational countries in the MENA region:
+- Jordan (الأردن)
+- Iraq (العراق)
+- Egypt (مصر)
+
+If you test the app on a physical device or emulator located outside these three countries (e.g. from the United States or Europe) with local GPS enabled, the camera will center on your local coordinates where our regional map tiles are not rendered, resulting in an empty tile canvas. This is expected behavior due to localized regional hosting.
+
+2. How to Test and Review the App Successfully:
+
+OPTION A (Recommended - Instant In-App Search):
+1. Launch the app and grant or dismiss location permissions.
+2. In the top search bar, type any landmark or city within our coverage area:
+ - For Jordan: Type "Amman" or "عمان" or "City Mall"
+ - For Iraq: Type "Baghdad" or "بغداد" or "Tahrir Square"
+ - For Egypt: Type "Cairo" or "القاهرة" or "Nasr City"
+3. Select any result. The map will instantly fly to the location, load rich vector tiles, 3D buildings, and gates.
+4. Tap "Start Navigation" or "ابدأ الملاحة" to experience turn-by-turn routing, Arabic voice guidance, and vehicle markers.
+
+OPTION B (Simulated GPS Location):
+In your emulator or testing device, set a mock GPS location to any of our operational hubs:
+- Amman (Jordan): Latitude 31.9539, Longitude 35.9106
+- Baghdad (Iraq): Latitude 33.3152, Longitude 44.3661
+- Cairo (Egypt): Latitude 30.0444, Longitude 31.2357
+The app will immediately render full street networks and navigation HUD.
+
+3. Authentication & Credentials:
+No username or password is required. The application automatically registers via secure hardware device fingerprinting and provisions a consumer API key seamlessly on initial launch.
+```
+
diff --git a/apps/siro_maps/android/app/build.gradle.kts b/apps/siro_maps/android/app/build.gradle.kts
index 8497e77..b0ef060 100644
--- a/apps/siro_maps/android/app/build.gradle.kts
+++ b/apps/siro_maps/android/app/build.gradle.kts
@@ -17,7 +17,7 @@ if (keystorePropertiesFile.exists()) {
android {
namespace = "com.urukmap.app"
compileSdk = flutter.compileSdkVersion
- ndkVersion = flutter.ndkVersion
+ ndkVersion = "28.2.13676358"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
@@ -64,6 +64,10 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
+
+ ndk {
+ debugSymbolLevel = "none"
+ }
}
debug {
signingConfig = signingConfigs.getByName("debug")
diff --git a/apps/siro_maps/android/app/proguard-rules.pro b/apps/siro_maps/android/app/proguard-rules.pro
index a26d94a..5d9638a 100644
--- a/apps/siro_maps/android/app/proguard-rules.pro
+++ b/apps/siro_maps/android/app/proguard-rules.pro
@@ -63,3 +63,13 @@
-dontwarn kotlin.**
-dontwarn kotlinx.coroutines.**
-keep class kotlin.Metadata { *; }
+
+# ── 6. PLAY STORE CORE & DEFERRED COMPONENTS ──────────────────────────────────
+-dontwarn com.google.android.play.core.**
+-dontwarn com.google.android.play.core.splitcompat.**
+-dontwarn com.google.android.play.core.splitinstall.**
+-dontwarn com.google.android.play.core.tasks.**
+
+# ── 7. GENERAL DEPENDENCIES ───────────────────────────────────────────────────
+-dontwarn javax.annotation.**
+-dontwarn org.checkerframework.**
diff --git a/apps/siro_maps/android/app/src/main/AndroidManifest.xml b/apps/siro_maps/android/app/src/main/AndroidManifest.xml
index 00e9981..ae751ef 100644
--- a/apps/siro_maps/android/app/src/main/AndroidManifest.xml
+++ b/apps/siro_maps/android/app/src/main/AndroidManifest.xml
@@ -3,12 +3,13 @@
-
-
-
+
+
+
+
= 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}';
diff --git a/apps/siro_maps/lib/core/services/telemetry_tracking_service.dart b/apps/siro_maps/lib/core/services/telemetry_tracking_service.dart
new file mode 100644
index 0000000..dde3d58
--- /dev/null
+++ b/apps/siro_maps/lib/core/services/telemetry_tracking_service.dart
@@ -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 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 _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 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 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 flush({MapSaasRepository? repository}) async {
+ if (_buffer.isEmpty || _isFlushing) return true;
+
+ _isFlushing = true;
+ final repo = repository ?? MapSaasRepository();
+ final pointsToSend = List.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 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;
+ }
+}
diff --git a/apps/siro_maps/lib/data/repositories/map_saas_repository.dart b/apps/siro_maps/lib/data/repositories/map_saas_repository.dart
index 1dda967..99fa6da 100644
--- a/apps/siro_maps/lib/data/repositories/map_saas_repository.dart
+++ b/apps/siro_maps/lib/data/repositories/map_saas_repository.dart
@@ -349,4 +349,35 @@ class MapSaasRepository {
return false;
}
}
+
+ /// Batch ingest multiple telemetry points in a single HTTP request (POST /api/telemetry/batch)
+ Future sendTelemetryBatch({
+ required List