Update: 2026-06-10 18:11:50

This commit is contained in:
Hamza-Ayed
2026-06-10 18:11:50 +03:00
parent a0473a8b0f
commit 977adfe99d
27 changed files with 3946 additions and 206 deletions

View File

@@ -0,0 +1,256 @@
# 🚗 دورة حياة الرحلة - تطبيق السائق (Driver Ride Lifecycle)
<div dir="rtl" align="right">
هذا الملف هو مرجع هندسي شامل ودقيق (Source of Truth) يوضح كافة تفاصيل دورة حياة الرحلة (Ride Lifecycle) في تطبيق السائق (Siro Driver). تمت كتابته وتوثيقه ليكون دليلاً استرشادياً كاملاً لأي مهندس برمجي أو نموذج ذكاء اصطناعي (AI Agent) يرغب في فهم أو تعديل منطق الرحلات، تتبع المواقع، أو معالجة الحالات الفعالة.
</div>
---
## 1. بنية مزود الموقع المركزي (LocationController)
<div dir="rtl" align="right">
يعد الكنترولر
[LocationController](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/functions/location_controller.dart)
هو المصدر الوحيد والمركزى (Single Source of Truth) لكافة تحديثات نظام التموضع العالمي (GPS). يُمنع منعاً باتاً إنشاء اشتراكات أو فتح قنوات استماع موازية مع Geolocator أو Location في أي مكان آخر.
</div>
### 1.1 التهيئة والتحكم في دورة الحياة (Initialization & Lifecycle)
<div dir="rtl" align="right">
* <b>دالة التهيئة</b>:
</div>
```dart
Future<void> onInit() async
```
<div dir="rtl" align="right">
تقوم بالخطوات التالية:
1. تسجيل الكنترولر كمراقب لدورة حياة التطبيق العامة عبر `WidgetsBinding.instance.addObserver(this)`.
2. الاستماع للمفتاح `BoxName.statusDriverLocation` للتحقق من حالة السائق (مثال: محظور `blocked`). إذا تم حظره، يتم إيقاف تتبع الموقع فوراً وتحديث السيرفر وفصل الاتصال.
3. انتظار تحميل الكنترولرات المعتمد عليها (`HomeCaptainController` و `CaptainWalletController`) عبر الدالة المساعدة `_awaitDependencies()`.
4. تهيئة الاتصال بالسوكيت عبر `initSocket()` وتهيئة إعدادات تحديد الموقع عبر `_initLocationSettings()` والبدء الفوري ببث الموقع في حال لم يكن السائق محظوراً.
</div>
<div dir="rtl" align="right">
* <b>مراقبة تغيير حالة التطبيق</b>:
</div>
```dart
void didChangeAppLifecycleState(AppLifecycleState state)
```
<div dir="rtl" align="right">
1. في حالة **Resumed** (التطبيق في الواجهة): يتم إيقاف تشغيل خدمة الخلفية للأندرويد عبر `BackgroundServiceHelper.stopService()`، وإعادة تهيئة السوكيت إذا تم قطعه.
2. في حالة **Paused** أو **Detached** (التطبيق في الخلفية): يتم حفظ الحالة محلياً وتشغيل خدمة الخلفية `BackgroundServiceHelper.startService()` لضمان استمرارية البث.
</div>
### 1.2 الاتصال بالويب سوكيت (WebSocket & Socket.io)
<div dir="rtl" align="right">
* <b>تهيئة السوكيت</b>:
</div>
```dart
void initSocket()
```
<div dir="rtl" align="right">
تقوم بفتح قناة اتصال مع خادم المواقع (`https://location.intaleq.xyz`) وتجهيز معلمات الاتصال (`driver_id`, `token`). تحتوي الدالة على حماية ضد الاستدعاءات المتداخلة (Debounce Flag: `_isInitializingSocket`).
</div>
<div dir="rtl" align="right">
* <b>إعداد المستمعين</b>:
</div>
```dart
void _setupSocketListeners()
```
<div dir="rtl" align="right">
تقوم بربط الأحداث القادمة من السيرفر كالتالي:
1. `connect`: تفعيل مؤشر الاتصال وتدشين الـ Heartbeat عبر الـ Timer.
2. `new_ride_request`: عند وصول طلب رحلة جديد، يتم معالجته فوراً وتوجيهه للواجهة عبر دالة `handleIncomingOrder()`.
3. `cancel_ride`: عند إلغاء الراكب للطلب، يتم إبلاغ `MapDriverController` فوراً لإيقاف وضع الملاحة والعودة للرئيسية عبر `processRideCancelledByPassenger()`.
</div>
<div dir="rtl" align="right">
* <b>إرسال الموقع الفوري للشبكة</b>:
</div>
```dart
void emitLocationToSocket(LatLng pos, double head, double spd)
```
<div dir="rtl" align="right">
تقوم بإرسال حزمة بيانات البث الفعلي (Latitude, Longitude, Heading, Speed, Distance, Status). في حال وجود رحلة نشطة بالحالة `Apply`, `Arrived`, أو `Begin` وبوجود معرف الراكب في الذاكرة، يتم حقن `passenger_id` و `ride_id` تلقائياً لتمكين الراكب من التتبع الحي.
</div>
### 1.3 معالجة الطلبات الواردة (Incoming Order Handler)
<div dir="rtl" align="right">
* <b>معالجة الطلب المستلم</b>:
</div>
```dart
Future<void> handleIncomingOrder(Map<String, dynamic> rideData, String source)
```
<div dir="rtl" align="right">
تتم معالجة الطلب بطريقتين بناءً على حالة التطبيق:
1. **خلفية التطبيق (Background)**:
* على **iOS**: يتم إرسال إشعار محلي فوري (Ding Tone) ليدعو السائق لفتح التطبيق.
* على **Android**: يتم إظهار واجهة التراكب السريعة (Overlay View).
2. **واجهة التطبيق (Foreground)**:
* يتم التحقق من عدم وجود رحلة جارية حالياً (Guard Clause: لمنع تداخل الأحداث أثناء تنفيذ رحلة نشطة).
* إخفاء أي تراكبات (Overlay Views) مفعلة.
* التوجيه الفوري للسائق نحو صفحة الطلب المخصصة `/OrderRequestPage` وتمرير معطيات الرحلة المستلمة.
</div>
### 1.4 الاستماع لتدفق الموقع المباشر (GPS Stream Listener)
<div dir="rtl" align="right">
* <b>الاستماع المباشر</b>:
</div>
```dart
Future<void> _subscribeLocationStream()
```
<div dir="rtl" align="right">
يقوم الكنترولر بالاستماع لتغيرات الموقع من الـ GPS، وبثها محلياً للجهات المعنية كالتالي:
1. تحديث إحداثيات السائق الحالية (`myLocation`) والسرعة والاتجاه وتخزينها محلياً في الـ Storage.
2. حساب تراكم المسافة الكلية المقطوعة (`totalDistance`).
3. تحديث خريطة السائق الرئيسية إذا كانت واجهة `HomeCaptain` نشطة.
4. إرسال الإحداثيات وتمريرها مباشرة إلى دالة `handleLocationUpdateFromCentral()` المتواجدة داخل `MapDriverController` (أثناء الذهاب للراكب أو الوجهة).
5. إرسال الإحداثيات وتمريرها مباشرة إلى دالة `handleLocationUpdateFromCentral()` المتواجدة داخل `NavigationController` (أثناء التوجيه خطوة بخطوة).
</div>
---
## 2. إدارة الرحلة النشطة (MapDriverController)
<div dir="rtl" align="right">
يتحكم
[MapDriverController](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/map_driver_controller.dart)
في مراحل تنفيذ الطلب بعد القبول، ورسم المسارات، وعمليات الوصول والإنهاء.
</div>
### 2.1 تحميل المعطيات واستعادة الحالة (Startup Argument Loading)
<div dir="rtl" align="right">
* <b>تحميل المعطيات</b>:
</div>
```dart
Future<void> argumentLoading()
```
<div dir="rtl" align="right">
تعمل هذه الدالة عند فتح الخريطة النشطة، وتقوم بالآتي:
1. قراءة بيانات الرحلة من المتغيرات المستلمة أو استعادتها من التخزين المحلي في حالات انقطاع التطبيق (`BoxName.rideArguments`).
2. تحليل ومعالجة إحداثيات الراكب والوجهة عبر دالة `latlng()`.
3. التحقق من حالة الرحلة المخزنة في الـ Box:
* إذا كانت الحالة **Begin**: تعني أن الرحلة قد بدأت بالفعل، فيتم تفعيل الأعلام (`isRideStarted = true` و `isRideBegin = true`)، ورسم خط المسار باللون **الأسود/الأزرق** باتجاه **الوجهة النهائية (Passenger Destination)**، وتشغيل عدادات الرحلة وتتبع الخطوات خطوة بخطوة.
* إذا كانت الحالة غير ذلك: يتم رسم خط المسار باللون **الأصفر** باتجاه **موقع الراكب (Passenger Location)**، وتشغيل محرك التوجيه لتتبع حركة السائق نحو نقطة الالتقاء.
</div>
### 2.2 تحديثات الموقع الواردة من المركز (Location Processing)
<div dir="rtl" align="right">
* <b>استقبال الموقع المركزي</b>:
</div>
```dart
void handleLocationUpdateFromCentral(LatLng newLoc, double posSpeed, double posHeading)
```
<div dir="rtl" align="right">
يتم استدعاء هذه الدالة دورياً من قبل `LocationController` عند كل إحداثي جديد:
1. استخدام آلية التنعيم الفوري لحركة الكاميرا والسيارة على الخريطة عبر الـ `AnimationController` لعمل interpolation سلس ومنع القفزات المفاجئة.
2. استدعاء فحص الملاحة والمسارات التفصيلية عبر `_checkNavigationStep()`.
3. التحقق من اقتراب السائق من وجهته عبر `checkDestinationProximity()`.
</div>
### 2.3 الانتقال بين حالات الرحلة (State Transitions)
<div dir="rtl" align="right">
الحالات كالتالي:
</div>
```
OrderRequestPage: Accept -> Apply -> PassengerLocationMapPage (Yellow Polyline) -> Arrived (Distance < 100m) -> Begin (Distance < 150m) -> Trip to Destination (Blue/Black Polyline) -> Finished -> RatePassenger Page
```
<div dir="rtl" align="right">
* <b>مرحلة وصول السائق للراكب (Arrive)</b>:
</div>
```dart
Future<void> markDriverAsArrived()
```
<div dir="rtl" align="right">
1. التحقق من المسافة الفاصلة بين السائق والراكب؛ يجب أن تكون أقل من 100 متر.
2. إرسال طلب تفعيل حالة الوصول لخادم الرحلات عبر `arrive_ride.php`.
3. بدء تشغيل مؤشر احتساب زمن انتظار السائق للراكب عبر `startTimerToShowDriverWaitPassengerDuration()`.
4. إعادة رسم المسار الجديد للوجهة مباشرة باللون الأزرق استعداداً للمرحلة القادمة.
</div>
<div dir="rtl" align="right">
* <b>مرحلة بدء الرحلة الفعلية (Begin)</b>:
</div>
```dart
Future<void> startRideFromDriver()
```
<div dir="rtl" align="right">
1. التحقق من قرب السائق من الراكب (يجب أن يكون أقل من 150 متر للسماح بالبدء).
2. إيقاف وإعادة ضبط مؤقتات انتظار الراكب.
3. كتابة حالة الرحلة محلياً في الـ Storage وتعيينها إلى `Begin`.
4. مسح الخطوط الصفراء السابقة ورسم مسار الرحلة الفعلي باللون **الأسود/الأزرق** للوجهة.
5. تشغيل عداد الرحلة النشط (`rideIsBeginPassengerTimer`) لتحديث عداد السعر التفاعلي.
6. تشغيل بث التوجيه والملاحة عبر `startListeningStepNavigation()`.
7. بدء تسجيل الصوت لتوثيق الرحلة لأسباب أمنية عبر `AudioRecorderController`.
8. إخطار السيرفر عبر API مخصص `start_ride.php` مع وجود منطق تراجع تلقائي (Rollback) في حال فشل الطلب لإعادة الحالة إلى `Apply`.
</div>
<div dir="rtl" align="right">
* <b>مرحلة إنهاء الرحلة بنجاح (Finish)</b>:
</div>
```dart
Future<void> finishRideFromDriver({bool isFromSlider = false})
Future<void> finishRideFromDriver1({bool isFromSlider = false})
```
<div dir="rtl" align="right">
1. **فحص التحقق المادي للرحلة**: حساب المسافة الإجمالية المخططة للرحلة، والتحقق من أن المسافة التي قطعتها السيارة بالفعل تتجاوز خمس (1/5) المسافة الكلية المطلوبة لمنع الإغلاقات الوهمية أو الخاطئة.
2. في حال مطابقة الشروط، يتم إيقاف تسجيل الصوت وحفظ الملف.
3. تحديث الحالات محلياً في الـ Storage إلى `Finished` وإيقاف تحديثات الموقع وإزالة معرفات الراكب المؤقتة.
4. إرسال حزمة بيانات موحدة ومتكاملة تشتمل على المسافة والوقت والتحقق المالي وخيارات المحفظة لخادم الرحلات عبر API المعاملات الموحد `finish_ride_updates.php`.
5. معالجة وتحديث محفظة السائق بالكامل وإرسال تقرير السلوك الفردي للسائق عبر `DriverBehaviorController`.
6. التوجيه النهائي لصفحة تقييم الراكب `RatePassenger` وتمرير الفاتورة المالية المحسوبة بدقة من السيرفر.
</div>
---
## 3. التوجيه الصوتي والملاحة التفصيلية (Navigation Engine)
<div dir="rtl" align="right">
* <b>الاستماع لتدفق خط الملاحة</b>:
</div>
```dart
void checkForNextStep(LatLng currentPosition)
void _checkNavigationStep(LatLng pos)
```
<div dir="rtl" align="right">
تقوم بمطابقة الموقع الحالي للسائق مع قائمة النقاط والخطوات التوجيهية للرحلة:
1. تحديد الخطوة والمناورة التوجيهية الحالية بناءً على النقطة الأقرب.
2. إذا اقترب السائق من نقطة الانعطاف القادمة (أقل من 30-50 متر)، يتم تفعيل الخطوة التالية وتحديث النصوص الإرشادية.
3. استدعاء محرك التوجيه الصوتي `TextToSpeechController` لنطق التوجيه الجديد باللغة العربية تلقائياً لمنع تشتت السائق أثناء القيادة.
4. قص الأجزاء التي تم قطعها وتجاوزها من المسار المرسوم عبر `_updateTraveledPolylineSmart()` للحفاظ على وضوح ونظافة خط الرحلة على الخريطة.
</div>

View File

@@ -76,15 +76,22 @@ class FirebaseMessagesController extends GetxController {
await fcmToken.subscribeToTopic("drivers"); // أو "users" حسب نوع المستخدم
print("Subscribed to 'drivers' topic ✅");
FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage? message) async {
FirebaseMessaging.instance
.getInitialMessage()
.then((RemoteMessage? message) async {
if (message != null && message.data.isNotEmpty) {
Log.print("🔔 FCM getInitialMessage payload: ${message.data}");
String? category = message.data['category'] ?? message.data['type'];
if (category == 'ORDER' || category == 'Order' || category == 'OrderVIP' || message.data.containsKey('DriverList')) {
if (category == 'ORDER' ||
category == 'Order' ||
category == 'OrderVIP' ||
message.data.containsKey('DriverList')) {
String? myListString = message.data['DriverList'];
if (myListString != null && myListString.isNotEmpty) {
await storage.write(key: 'pending_driver_list', value: myListString);
Log.print("💾 Saved pending driver list to secure storage from getInitialMessage");
await storage.write(
key: 'pending_driver_list', value: myListString);
Log.print(
"💾 Saved pending driver list to secure storage from getInitialMessage");
}
} else {
Future.delayed(const Duration(milliseconds: 1500), () {
@@ -107,7 +114,6 @@ class FirebaseMessagesController extends GetxController {
// fireBaseTitles(message);
// }
});
FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async {});
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
if (message.data.isNotEmpty) {

View File

@@ -9,7 +9,6 @@ import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:socket_io_client/socket_io_client.dart' as IO;
import 'package:flutter_overlay_window/flutter_overlay_window.dart' as Overlay;
import 'package:get_storage/get_storage.dart';
import 'package:geolocator/geolocator.dart' as geo;
import '../../constant/box_name.dart';
import '../firebase/local_notification.dart';
@@ -129,40 +128,21 @@ Future<bool> onStart(ServiceInstance service) async {
service.stopSelf();
});
// 🔥 Location management in background isolate (Using Geolocator)
geo.Position? latestPos;
// Listen to location changes continuously in the background
geo.Geolocator.getPositionStream(
locationSettings: geo.AndroidSettings(
accuracy: geo.LocationAccuracy.high,
distanceFilter: 10,
intervalDuration: const Duration(seconds: 10),
),
).listen((pos) {
latestPos = pos;
});
// 🔥 MERCY HEARTBEAT: Send location every 2 minutes to keep driver active in 'raids'
Timer.periodic(const Duration(minutes: 2), (timer) async {
if (socket != null && socket.connected && latestPos != null) {
try {
socket.emit('update_location', {
'driver_id': driverId,
'lat': latestPos!.latitude,
'lng': latestPos!.longitude,
'heading': latestPos!.heading,
'speed': latestPos!.speed * 3.6,
'status': box.read(BoxName.statusDriverLocation) ?? 'on',
'source': 'background_heartbeat'
});
print(
"💓 Background Mercy Heartbeat Sent: ${latestPos!.latitude}, ${latestPos!.longitude}");
} catch (e) {
print("❌ Background Heartbeat Error: $e");
}
}
});
// 🚫 [Architecture Rule] NO redundant GPS stream in background service!
// LocationController is the SINGLE SOURCE OF TRUTH for all location GPS updates.
// It already uses location.enableBackgroundMode(enable: true) to keep the GPS
// stream alive even when the app is in the background. The main socket in
// LocationController handles all emitLocationToSocket() calls including heartbeat.
//
// The background service is ONLY responsible for:
// 1. Keeping the socket connection alive for receiving 'new_ride_request'
// and 'cancel_ride' events while the main isolate is paused on Android.
// 2. Showing the Android Overlay UI for incoming ride requests.
// 3. Notifications for iOS background state.
//
// Location data is not sent from the background isolate — it would conflict
// with LocationController's stream and cause duplicate GPS listeners,
// battery drain, and device freeze (as documented in driver_lifecycle.md).
Timer.periodic(const Duration(seconds: 30), (timer) async {
if (service is AndroidServiceInstance) {

View File

@@ -19,6 +19,7 @@ import '../firebase/local_notification.dart';
import '../home/captin/home_captain_controller.dart';
import '../home/captin/map_driver_controller.dart';
import '../home/payment/captain_wallet_controller.dart';
import '../home/navigation/navigation_controller.dart';
import 'background_service.dart';
import 'crud.dart';
@@ -539,6 +540,16 @@ class LocationController extends GetxController with WidgetsBindingObserver {
}
}
if (Get.isRegistered<MapDriverController>()) {
final mapCtrl = Get.find<MapDriverController>();
mapCtrl.handleLocationUpdateFromCentral(pos, speed, heading);
}
if (Get.isRegistered<NavigationController>()) {
final navCtrl = Get.find<NavigationController>();
navCtrl.handleLocationUpdateFromCentral(pos, speed, heading);
}
await _saveBehaviorIfMoved(pos, now, currentSpeed: speed);
}, onError: (e) => Log.print('❌ Location Stream Error: $e'));
}

View File

@@ -2570,27 +2570,19 @@ class MapDriverController extends GetxController
}
void _startLocationListening() {
_locationSubscription?.cancel();
_locationSubscription = geo.Geolocator.getPositionStream(
locationSettings: const geo.LocationSettings(
accuracy: geo.LocationAccuracy.bestForNavigation,
distanceFilter: 2,
),
).listen((geo.Position pos) {
_handleLocationUpdate(pos);
});
// Location stream is now centralized in LocationController to prevent device hanging.
// LocationController will call handleLocationUpdateFromCentral directly.
}
/// [Fix C-4] تحديث myLocation في المستمع الأساسي
void _handleLocationUpdate(geo.Position pos) {
final newLoc = LatLng(pos.latitude, pos.longitude);
void handleLocationUpdateFromCentral(LatLng newLoc, double posSpeed, double posHeading) {
myLocation = newLoc; // ← [Fix C-4] تحديث الموقع الفوري
_oldLoc = smoothedLocation ?? newLoc;
_targetLoc = newLoc;
_oldHeading = smoothedHeading;
if (pos.speed > 0.5) {
_targetHeading = pos.heading;
if (posSpeed > 0.5) {
_targetHeading = posHeading;
} else {
_targetHeading = _oldHeading;
}

View File

@@ -69,6 +69,7 @@ class OrderRequestController extends GetxController
// --- الخريطة ---
Set<Polyline> polylines = {};
bool _hasCalculatedFullJourney = false;
// حالة التطبيق والصوت
bool isInBackground = false;
@@ -219,6 +220,11 @@ class OrderRequestController extends GetxController
// ----------------------------------------------------------------------
Future<void> _calculateFullJourney() async {
if (_hasCalculatedFullJourney) {
if (mapController != null) zoomToFitRide();
return;
}
_hasCalculatedFullJourney = true;
// Don't block on mapController being null - we'll draw routes
// and markers first, then zoom when controller is ready
bool canZoom = mapController != null;
@@ -281,7 +287,7 @@ class OrderRequestController extends GetxController
totalTripDistance = tripResult['distance_text'];
totalTripDuration = tripResult['duration_text'];
polylines.add(tripResult['polyline']);
// 🔥 تخزين استجابة السيرفر كاملة (بما فيها الـ points والـ instructions)
if (tripResult['raw_response'] != null) {
box.write('cached_trip_route', tripResult['raw_response']);

View File

@@ -476,32 +476,18 @@ class NavigationController extends GetxController
}
void _startLocationStream() {
_locationStreamSubscription?.cancel();
// Listen to location updates with minimum distance filter of 2 meters
// This provides real-time updates without the 3-4 second delay
_locationStreamSubscription = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 2, // Update every 2 meters
),
).listen(
(Position position) {
_handleLocationUpdate(position);
},
onError: (error) {
Log.print("DEBUG: Location stream error: $error");
},
);
// Location stream is now centralized in LocationController to prevent device hanging.
// LocationController will call handleLocationUpdateFromCentral directly.
}
bool _isProcessing = false;
Future<void> _handleLocationUpdate(Position position) async {
Future<void> handleLocationUpdateFromCentral(LatLng newLoc, double locSpeed, double locHeading) async {
if (_isProcessing) return;
_isProcessing = true;
try {
final newLoc = LatLng(position.latitude, position.longitude);
currentSpeed = position.speed * 3.6; // Convert m/s to km/h
currentSpeed = locSpeed; // Convert m/s to km/h already done by location controller if needed, wait location_controller sends raw speed or km/h? It sends raw speed. So we should * 3.6
currentSpeed = locSpeed * 3.6;
// Skip if movement is too small
if (_lastProcessedLocation != null) {
@@ -544,7 +530,7 @@ class NavigationController extends GetxController
_targetLoc!.longitude,
);
} else {
_targetHeading = position.heading;
_targetHeading = locHeading;
}
_animController?.forward(from: 0.0);