Update: 2026-07-04 16:47:10

This commit is contained in:
Hamza-Ayed
2026-07-04 16:47:11 +03:00
parent c8fdd1bb5a
commit 838e5ec656
4 changed files with 534 additions and 2 deletions
+440
View File
@@ -0,0 +1,440 @@
# قائمة التحقق من الامتثال لـ Android Auto و Apple CarPlay - تطبيق Siro Driver
## الرموز التوضيحية
- ✅ **مكتمل** — تم التنفيذ في الكود الحالي
- 📋 **يدوي** — يجب تنفيذه خارج الكود (App Store Connect، Play Console، Apple Developer)
- ⚠️ **يحتاج تحقق** — يحتاج اختبار على أجهزة حقيقية قبل الإطلاق
---
## ANDROID AUTO (`siro_driver`)
### 1. AndroidManifest.xml
| المتطلب | الحالة | الملف | ملاحظات |
|---|---|---|---|
| `androidx.car.app.CarAppService` مع intent-filter | ✅ | `AndroidManifest.xml:142-148` | مُعلن مع `exported="true"` |
| `<meta-data>` للسيارات يشير إلى `automotive_app_desc.xml` | ✅ | `AndroidManifest.xml:72-74` | `com.android.car.meta` → `@xml/automotive_app_desc` |
| أذونات الموقع المطلوبة (FINE، COARSE، BACKGROUND) | ✅ | `AndroidManifest.xml:5-7` | جميع الأذونات الثلاثة موجودة |
| إذن `FOREGROUND_SERVICE` | ✅ | `AndroidManifest.xml:8-10` | نوع location |
| `FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING` | ✅ | `AndroidManifest.xml:10` | لـ FCM |
| إذن الإشعارات (API 33+) | ⚠️ | `main.dart:60-81` | طلب وقت التشغيل موجود؛ تحقق على Android 14+ |
### 2. موارد السيارات (Automotive Resources)
| المتطلب | الحالة | الملف | ملاحظات |
|---|---|---|---|
| `automotive_app_desc.xml` مع `<uses name="navigation" />` | ✅ | `res/xml/automotive_app_desc.xml` | يعلن فئة التنقل بشكل صحيح |
### 3. Gradle / التبعيات (Dependencies)
| المتطلب | الحالة | الملف | ملاحظات |
|---|---|---|---|
| `androidx.car.app:app:1.4.0` | ✅ | `app/build.gradle:118` | أحدث إصدار مستقر |
| `compileSdk 36` | ✅ | `app/build.gradle:38` | يلبي الحد الأدنى لمكتبة Car App |
| `minSdk 30` | ✅ | `app/build.gradle:67` | كافٍ لـ Android Auto |
### 4. خدمة تطبيق السيارة (Car App Service)
| المتطلب | الحالة | الملف | ملاحظات |
|---|---|---|---|
| فئة فرعية من `CarAppService` | ✅ | `MyCarAppService.kt` | تنشئ `MyCarSession` |
| `createHostValidator()` | ✅ | `MyCarAppService.kt:9-18` | `ALLOW_ALL` في الوضع التجريبي، allowlist في الإنتاج |
| `onCreateSession()` | ✅ | `MyCarAppService.kt:21-23` | تُرجع `MyCarSession` |
### 5. الجلسة (Session)
| المتطلب | الحالة | الملف | ملاحظات |
|---|---|---|---|
| فئة فرعية من `Session` مع `onCreateScreen()` | ✅ | `MyCarSession.kt` | تُرجع `MyCarScreen` |
| `SurfaceCallback` لـ VirtualDisplay | ✅ | `MyCarSession.kt:24-61` | ينشئ VirtualDisplay لـ MapLibre |
| مراقب دورة الحياة لإيقاف/استئناف الخريطة | ✅ | `MyCarSession.kt:66-68` | `onResume`/`onPause`/`onDestroy` |
### 6. الشاشة / قالب التنقل (Screen / Navigation Template)
| المتطلب | الحالة | الملف | ملاحظات |
|---|---|---|---|
| `NavigationTemplate` مع تعليمات منعطف-بمنعطف | ✅ | `MyCarScreen.kt` | يعرض الخطوة الحالية، المناورة، المسافة |
| تعيين `Maneuver` من أكواد Siro | ✅ | `MyCarScreen.kt:66-79` | يربط جميع 8 أنواع مناورات |
| `RoutingInfo` مع الخطوة الحالية | ✅ | `MyCarScreen.kt:46-48` | |
| شاشة انتظار عند عدم التنقل | ✅ | `MyCarScreen.kt:26-31` | `MessageTemplate` |
| شريط إجراءات مع `APP_ICON` | ✅ | `MyCarScreen.kt:53-57` | |
### 7. عرض الخريطة (Map Display)
| المتطلب | الحالة | الملف | ملاحظات |
|---|---|---|---|
| عرض MapLibre على VirtualDisplay | ✅ | `MapPresentation.kt` | خريطة بملء الشاشة على شاشة السيارة |
| الكاميرا تتبع GPS | ✅ | `MapPresentation.kt:71-107` | زوم/ميل متكيف مع السرعة |
### 8. جسر بيانات التنقل (Navigation Data Bridge)
| المتطلب | الحالة | الملف | ملاحظات |
|---|---|---|---|
| قناة `car_navigation` MethodChannel على الـ Native | ✅ | `MainActivity.kt:66-107` | تعالج `updateNavState`، `updateLocation`، `updateInstruction`، `stopNavigation` |
| `CarNavigationData` singleton بنمط المستمع (listener pattern) | ✅ | `CarNavigationData.kt` | نمط المستمع آمن لل threads |
| مكالمات جسر Dart→Native | ✅ | `NavigationController.dart` | كل حدث موقع/خطوة/تنقل يدفع إلى القناة |
### 9. خدمة المقدمة (Foreground Service)
| المتطلب | الحالة | الملف | ملاحظات |
|---|---|---|---|
| خدمة خلفية مع نوع location | ✅ | `AndroidManifest.xml:124-129` | `id.flutter.flutter_background_service.BackgroundService` |
| خدمة تحديث الموقع | ✅ | `AndroidManifest.xml:135-136` | `.LocationUpdatesService` |
### 10. متطلبات متجر Play
| المتطلب | الحالة | ملاحظات |
|---|---|---|
| إعلان فئة التنقل | ✅ | `automotive_app_desc.xml` |
| التطبيق موقع بمفتاح إصدار | ✅ | `key.properties` مضبوط |
| MinSDK ≥ 30 | ✅ | |
| قارئ الشاشة / إمكانية الوصول | ⚠️ | تحقق من عمل TalkBack مع NavigationTemplate |
| لقطة شاشة Android Auto | 📋 | ارفع في Play Console تحت قسم "Android Auto" |
| مراجعة إرشادات جودة Android Auto | 📋 | راجع https://developer.android.com/training/cars |
---
## APPLE CARPLAY (`siro_driver`)
### 1. الاستحقاقات (Entitlements)
| المتطلب | الحالة | الملف | ملاحظات |
|---|---|---|---|
| `com.apple.developer.carplay-driving-task` | ✅ | `Runner.entitlements:7-8` | مطلوب لتطبيقات التنقل |
| `aps-environment` (push) | ✅ | `Runner.entitlements:6-7` | موجود مسبقاً |
### 2. Info.plist
| المتطلب | الحالة | الملف | ملاحظات |
|---|---|---|---|
| `CPApplication` مع `CPApplicationDriverManeuver` | ✅ | `Info.plist:98-111` | `supportsNavigation: true` |
| `UIBackgroundModes` مع `location` | ✅ | `Info.plist:117` | موجود مسبقاً |
| `NSLocationAlwaysAndWhenInUseUsageDescription` | ✅ | `Info.plist:83-85` | موجود مسبقاً |
### 3. AppDelegate / اتصال CarPlay
| المتطلب | الحالة | الملف | ملاحظات |
|---|---|---|---|
| `application(_:didConnectCarInterfaceController:to:)` | ✅ | `AppDelegate.swift:43-58` | ينشئ `CPMapTemplate` |
| `application(_:didDisconnectCarInterfaceController:from:)` | ✅ | `AppDelegate.swift:60-67` | ينظف التنقل |
| قناة `setupCarNavigationChannel()` Method Channel | ✅ | `AppDelegate.swift:89-167` | تعالج كل تحديثات التنقل |
| فئة مساعدة `CarPlaySceneDelegate` | ✅ | `CarPlaySceneDelegate.swift` | جلسة تنقل، مناورات، وصول |
### 4. قوالب التنقل (Navigation Templates)
| المتطلب | الحالة | الملف | ملاحظات |
|---|---|---|---|
| `CPMapTemplate` كقالب جذر | ✅ | `CarPlaySceneDelegate.swift:6` | |
| `startNavigationSession(for:)` | ✅ | `CarPlaySceneDelegate.swift:27-38` | ينشئ `CPTrip` مع origem/destination |
| `CPManeuver` مع متغيرات التعليمات | ✅ | `CarPlaySceneDelegate.swift:44-59` | |
| تعيين ألوان المناورة | ✅ | `CarPlaySceneDelegate.swift:116-124` | أخضر للوصول، أزرق للمنعرجات |
| تحديث تقديرات الوصول | ✅ | `CarPlaySceneDelegate.swift:65-75` | `updatingArrival(to:)` |
| إلغاء التنقل عند الإيقاف | ✅ | `CarPlaySceneDelegate.swift:86-93` | `session.cancel()` |
### 5. جسر بيانات التنقل (Navigation Data Bridge)
| المتطلب | الحالة | ملاحظات |
|---|---|---|
| `updateNavState` → مزامنة تنقل كاملة | ✅ | ينشئ/يحدث `CPNavigationSession` |
| `updateLocation` → تحريك الخريطة | ✅ | يعرض واجهة التحريك (panning interface) |
| `updateInstruction` → تحديث المناورة | ✅ | يحدث `CPManeuver` |
| `stopNavigation` → إلغاء الجلسة | ✅ | ينظف الموارد |
### 6. التنفيذ في الخلفية (Background Execution)
| المتطلب | الحالة | ملاحظات |
|---|---|---|
| وضع خلفية الموقع | ✅ | ممكّن مسبقاً |
| إذن الموقع الدائم (Always) | ✅ | موجود مسبقاً |
| CarPlay يعمل في عملية خاصة به | ✅ | تديره iOS تلقائياً |
### 7. متطلبات متجر التطبيقات (App Store Requirements)
| المتطلب | الحالة | ملاحظات |
|---|---|---|
| استحقاق CarPlay في ملف التوفير (provisioning profile) | 📋 | يجب إضافته في Apple Developer → Certificates, Identifiers & Profiles |
| معرف التطبيق (App ID) به قدرة CarPlay مفعلة | 📋 | فعّل في Apple Developer Portal للمعرف (bundle ID) |
| Xcode Capability: CarPlay (Driving Task) | 📋 | تأكد في Xcode Signing & Capabilities |
| لقطات شاشة CarPlay لمتجر التطبيقات | 📋 | لقطات 6.7" و 5.5" مع واجهة CarPlay |
| أيقونة تطبيق CarPlay (إذا مطلوبة) | 📋 | أيقونة تطبيق CarPlay (40pt @2x/3x) — اختياري لتطبيقات التنقل |
| مراجعة إرشادات واجهة CarPlay (HIG) | 📋 | https://developer.apple.com/carplay/ |
---
## DART / FLUTTER (الجسر المشترك)
| المتطلب | الحالة | الملف | ملاحظات |
|---|---|---|---|
| `CarPlatformBridge` مُنشأ | ✅ | `lib/controller/car_platform_bridge.dart` | دوال ثابتة لكلا المنصتين |
| الجسر مهيأ في `main()` | ✅ | `main.dart:269` | بعد خدمة الخلفية |
| الجسر مهيأ في `NavigationController.onInit()` | ✅ | `NavigationController.dart:304` | زائد لكن آمن |
| حالة التنقل تُدفع عند تحديث الموقع | ✅ | `NavigationController.dart:552` | عبر `_pushCarBridgeUpdate()` |
| حالة التنقل تُدفع عند `startActiveNavigation()` | ✅ | `NavigationController.dart:1141` | |
| تعليمات تُدفع عند `_advanceStep()` | ✅ | `NavigationController.dart:1235-1241` | |
| إيقاف عند `_finishNavigation()` | ✅ | `NavigationController.dart:1259` | |
| إيقاف عند `clearRoute()` | ✅ | `NavigationController.dart:1164` | |
---
## الخطوات اليدوية المطلوبة (MANUAL STEPS REQUIRED)
### قبل الإرسال (Pre-Submission)
- [ ] **Apple Developer Portal**: أضف قدرة `CarPlay (Driving Task)` لمعرف التطبيق
- [ ] **Apple Developer Portal**: أعد إنشاء ملفات التوفير بعد تفعيل CarPlay
- [ ] **Xcode**: فعّل قدرة CarPlay في Signing & Capabilities
- [ ] **Google Play Console**: أعلن تكامل Android Auto → اضبط فئة "Navigation"
- [ ] **Google Play Console**: ارفع لقطات شاشة Android Auto (مطلوبة للإدراج)
- [ ] **التوقيع**: يجب أن يستخدم كلا النظامين توقيع الإنتاج/التوزيع
### الاختبار (Testing)
- [ ] **Android Auto**: اختبر مع محاكي DHU (Desktop Head Unit)
- [ ] **Android Auto**: اختبر على وحدة رأس سيارة حقيقية مع USB
- [ ] **Apple CarPlay**: اختبر مع محاكي CarPlay في Xcode
- [ ] **Apple CarPlay**: اختبر على سيارة حقيقية مع كابل Lightning/USB-C
- [ ] **الإرشاد الصوتي**: تحقق من عمل تكامل TTS أثناء نشاط CarPlay/Android Auto
- [ ] **إعادة التوجيه**: تحقق من انعكاس إعادة الحساب التلقائي على شاشة السيارة
- [ ] **نهاية الرحلة**: تحقق من انتهاء جلسة التنقل بنظافة على المنصتين
### الامتثال (Compliance)
- [ ] **إرشادات جودة Android Auto**: https://developer.android.com/training/cars
- [ ] **إرشادات واجهة CarPlay (HIG) من Apple**: https://developer.apple.com/carplay/
- [ ] **الخصوصية**: تأكد من توثيق استخدام بيانات الموقع بوضوح في سياسة الخصوصية
---
## الملخص (SUMMARY)
| المنصة | الحالة | الملفات المعدلة/المضافة |
|---|---|---|
| **Android Auto** | ✅ جاهز للإنتاج | `AndroidManifest.xml` (إصلاح)، `car_platform_bridge.dart` (جديد)، `NavigationController.dart` (توصيل)، `main.dart` (تهيئة) |
| **Apple CarPlay** | ✅ جاهز للإنتاج | `CarPlaySceneDelegate.swift` (جديد)، `AppDelegate.swift` (CarPlay + القناة)، `Info.plist` (CPApplication)، `Runner.entitlements` (carplay-driving-task)، `car_platform_bridge.dart` (مشترك)، `NavigationController.dart` (توصيل)، `main.dart` (تهيئة) |
---
## تفاصيل التنفيذ التقنية (Technical Implementation Details)
### Android Auto - البنية التحتية (Architecture)
```
┌─────────────────────────────────────────────────────────────────┐
│ السائق (Driver App) │
│ ┌─────────────────┐ MethodChannel ┌──────────────────┐ │
│ │ NavigationController │◄─────────────────►│ MainActivity.kt │ │
│ │ (Flutter/Dart) │ car_navigation │ (Native/Kotlin) │ │
│ └────────┬──────────┘ └────────┬─────────┘ │
│ │ │ │
│ │ _pushCarBridgeUpdate() │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ CarNavigationData (Kotlin Singleton) │ │
│ │ - currentLat/Lng/Bearing/Speed │ │
│ │ - currentInstruction, distanceToStep, ETA, maneuverType │ │
│ │ - isNavigating │ │
│ │ - Listener pattern للـ UI updates │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ MyCarScreen.kt │ │ MapPresentation │ │
│ │ (NavigationTemplate) │ (MapLibre on │ │
│ │ - يعرض الخطوة الحالية │ VirtualDisplay)│ │
│ │ - المناورة، المسافة │ - كاميرا تتبع GPS│ │
│ │ - شريط إجراءات │ - زوم/ميل متكيف │ │
│ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
**ملفات Android Auto الرئيسية:**
- `MyCarAppService.kt` — نقطة دخول CarAppService
- `MyCarSession.kt` — يدير دورة الحياة و VirtualDisplay
- `MyCarScreen.kt` — يبني NavigationTemplate
- `MapPresentation.kt` — يعرض MapLibre على شاشة السيارة
- `CarNavigationData.kt` — مخزن بيانات مشترك آمن للـ threads
- `MainActivity.kt` — جسر MethodChannel
### Apple CarPlay - البنية التحتية (Architecture)
```
┌─────────────────────────────────────────────────────────────────┐
│ السائق (Driver App) │
│ ┌─────────────────┐ MethodChannel ┌──────────────────┐ │
│ │ NavigationController │◄─────────────────►│ AppDelegate.swift │ │
│ │ (Flutter/Dart) │ car_navigation │ (Native/Swift) │ │
│ └────────┬──────────┘ └────────┬─────────┘ │
│ │ │ │
│ │ _pushCarBridgeUpdate() │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ CarPlaySceneDelegate (Singleton) │ │
│ │ - interfaceController: CPInterfaceController │ │
│ │ - mapTemplate: CPMapTemplate │ │
│ │ - navigationSession: CPNavigationSession? │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ CPMapTemplate │ │ CPTrip + │ │
│ │ (Root Template) │ │ CPNavigationSession│ │
│ │ - guidanceBackgroundStyle = .light │ - origin/dest │ │
│ │ - يعرض المناورات والـ ETA │ - maneuvers │ │
│ └─────────────────┘ │ - arrival estimates│ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
**ملفات CarPlay الرئيسية:**
- `AppDelegate.swift` — يتعامل مع الاتصال/فصل الاتصال، جسر MethodChannel
- `CarPlaySceneDelegate.swift` — يدير جلسة التنقل، المناورات، التقديرات
- `CarPlayState.swift` — singleton مشترك للوصول إلى الـ delegate
- `Info.plist` — يعلن `CPApplication` مع `supportsNavigation: true`
- `Runner.entitlements` — استحقاق `com.apple.developer.carplay-driving-task`
### جسر Flutter المشترك (Shared Flutter Bridge)
**`car_platform_bridge.dart`** — واجهة موحدة لكلا المنصتين:
```dart
class CarPlatformBridge {
static const _channel = MethodChannel('com.siro.siro_driver/car_navigation');
// تحديث حالة التنقل الكاملة (للبدء/التحديث)
static Future<void> updateNavState({
required double lat, lng, bearing, speed,
required String instruction,
required double distanceToStep, totalDistance, eta,
required int maneuver,
required bool isNavigating,
});
// تحديث الموقع فقط (للتحريك على الخريطة)
static Future<void> updateLocation({
required double lat, lng, bearing, speed,
});
// تحديث التعليمات/المناورة فقط
static Future<void> updateInstruction({
required String instruction,
required int maneuver,
required double distanceToStep,
});
// إيقاف التنقل
static Future<void> stopNavigation();
}
```
**تدفق البيانات في `NavigationController.dart`:**
```dart
// عند بدء التنقل النشط
void startActiveNavigation() {
isNavigating = true;
_pushCarBridgeUpdate(); // يرسل isNavigating: true
}
// عند كل تحديث موقع (كل ~4 ثوانٍ)
void _onLocationUpdate(Position pos) {
// ... معالجة الموقع ...
_pushCarBridgeUpdate(); // يرسل lat/lng/bearing/speed/instruction/ETA/maneuver
}
// عند التقدم لخطوة جديدة
void _advanceStep() {
// ... تحديث currentInstruction، currentManeuverModifier ...
CarPlatformBridge.updateInstruction(
instruction: currentInstruction,
maneuver: currentManeuverModifier,
distanceToStep: _distanceToNextStepMeters,
);
}
// عند إنهاء التنقل (وصول أو إلغاء)
void _finishNavigation() {
isNavigating = false;
CarPlatformBridge.stopNavigation(); // يرسل isNavigating: false
}
void clearRoute() {
// ...
CarPlatformBridge.stopNavigation();
}
```
### تعيين المناورات (Maneuver Mapping)
| كود Siro | Android Auto (`Maneuver.TYPE_*`) | CarPlay (`CPManeuver`) |
|---|---|---|
| 0 (مستقيم) | `TYPE_STRAIGHT` | سهم مستقيم |
| 2 (يمين) | `TYPE_TURN_NORMAL_RIGHT` | يمين |
| 3 (يمين خفيف) | `TYPE_TURN_SLIGHT_RIGHT` | يمين خفيف |
| -2 (يسار) | `TYPE_TURN_NORMAL_LEFT` | يسار |
| -1 (يسار خفيف) | `TYPE_TURN_SLIGHT_LEFT` | يسار خفيف |
| 4 (وصول) | `TYPE_DESTINATION` | أخضر (وصول) |
| 6 (دوار) | `TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW` | دوار |
| 7 (ابق يمين) | `TYPE_KEEP_RIGHT` | ابق يمين |
| -7 (ابق يسار) | `TYPE_KEEP_LEFT` | ابق يسار |
**الألوان على CarPlay:**
- النوع 4 (وصول) → أخضر نظامي (`systemGreen`)
- النوع 2، -2 (يمين/يسار) → أزرق نظامي (`systemBlue`)
- الباقي → لا لون خاص
---
## قائمة التحقق من الاختبار (Testing Checklist)
### Android Auto
| الاختبار | الحالة | ملاحظات |
|---|---|---|
| تشغيل التطبيق على DHU (Desktop Head Unit) | ⬜ | محاكي Android Auto |
| اتصال USB بوحدة رأس حقيقية | ⬜ | سيارة حقيقية أو وحدة رأس خارجية |
| بدء التنقل من التطبيق → يظهر على الشاشة | ⬜ | |
| تعليمات منعطف-بمنعطف تظهر بشكل صحيح | ⬜ | |
| المناورات (يمين/يسار/دوار/وصول) تعرض الأيقونات الصحيحة | ⬜ | |
| تحديثات ETA والمسافة تعمل | ⬜ | |
| الكاميرا تتبع الموقع على الخريطة (MapLibre) | ⬜ | |
| إعادة التوجيه التلقائي ينعكس على شاشة السيارة | ⬜ | |
| إيقاف التنقل ينظف الجلسة | ⬜ | |
| خدمة المقدمة تستمر في الخلفية | ⬜ | |
| TalkBack / إمكانية الوصول تعمل | ⬜ | |
### Apple CarPlay
| الاختبار | الحالة | ملاحظات |
|---|---|---|
| تشغيل على محاكي CarPlay في Xcode | ⬜ | iOS Simulator → I/O → External Displays → CarPlay |
| اتصال Lightning/USB-C بسيارة حقيقية | ⬜ | |
| بدء التنقل → يظهر CPMapTemplate | ⬜ | |
| المناورات مع ألوان (أخضر/أزرق) | ⬜ | |
| تقديرات الوصول (ETA) تُحدث | ⬜ | |
| واجهة التحريك (panning) تظهر عند تحديث الموقع | ⬜ | |
| الأوامر الصوتية / Siri تعمل | ⬜ | |
| إلغاء التنقل → `session.cancel()` | ⬜ | |
| التشغيل في الخلفية مع موقع دائم | ⬜ | |
| VoiceOver / إمكانية الوصول تعمل | ⬜ | |
---
## العناصر المتبقية للإنتاج (Production Readiness)
### Android Auto
1. ✅ كود التطبيق مكتمل
2. 📋 **Play Console**: إعلان فئة "Navigation" في قسم Android Auto
3. 📋 **Play Console**: رفع لقطات شاشة Android Auto (مطلوبة للنشر)
4. 📋 **التوقيع**: بناء موقع بمفتاح الإصدار (`key.properties`)
5. ⚠️ **اختبار**: تحقق على DHU ووحدة رأس حقيقية
6. ⚠️ **إمكانية الوصول**: تحقق TalkBack مع NavigationTemplate
### Apple CarPlay
1. ✅ كود التطبيق مكتمل
2. 📋 **Apple Developer Portal**: أضف قدرة `CarPlay (Driving Task)` للمعرف
3. 📋 **Apple Developer Portal**: أعد إنشاء ملفات التوفير
4. 📋 **Xcode**: فعّل قدرة CarPlay في Signing & Capabilities
5. 📋 **App Store Connect**: ارفع لقطات شاشة CarPlay (6.7" و 5.5")
6. 📋 **App Store Connect**: أيقونة CarPlay (اختياري للتنقل)
7. ⚠️ **اختبار**: محاكي CarPlay + سيارة حقيقية
8. ⚠️ **مراجعة HIG**: https://developer.apple.com/carplay/
---
## ملاحظات للمطورين (Developer Notes)
### لنقل الكود لتطبيق الراكب (Rider App) لاحقاً:
1. **Android Auto**: انسخ ملفات `MyCarAppService.kt`، `MyCarSession.kt`، `MyCarScreen.kt`، `MapPresentation.kt`، `CarNavigationData.kt`، وعدل `package` إلى `com.siro.siro_rider`
2. **iOS CarPlay**: انسخ `CarPlaySceneDelegate.swift`، عدّل `AppDelegate.swift` لإضافة جسر `car_navigation`، حدث `Info.plist` و `Runner.entitlements`
3. **Flutter**: `CarPlatformBridge.dart` مشترك، فقط تأكد من تهيئته في `main.dart` للراكب وربطه بـ `NavigationController` الخاص بالراكب
4. **الأذونات**: نفس أذونات الموقع والخلفية مطلوبة
### مشاكل معروفة / تحسينات مستقبلية:
- **CarPlay**: `updatingArrival(to:)` مهمل في iOS 15+، فكر في الهجرة لـ `updating(_:)` على `arrivalEstimates` للمناورة
- **Android Auto**: `HostValidator` في الإنتاج يجب أن يستخدم `hosts_allowlist_google` وليس `hosts_allowlist_sample`
- **MapLibre على CarPlay**: تأكد من أن `asset://flutter_assets/assets/style.json` متاح في bundle التطبيق على iOS أيضاً (قد يحتاج نسخ للأصول)
- **TTS على CarPlay/Android Auto**: حالياً TTS يعمل على الهاتف؛ للتحكم الصوتي عبر مكبرات السيارة، قد تحتاج `CPVoiceControlTemplate` أو تكامل `TTS` مع `CarAppService`
---
*تم إنشاء هذا التقرير بتاريخ: 2026-07-04*
*استناداً إلى: `docs/auto_carplay_checklist.md` (النسخة الإنجليزية) ومراجعة الكود الكامل لتطبيق `siro_driver`*
+86 -2
View File
@@ -302,8 +302,28 @@ $io->on('workerStart', function () use ($io, $INTERNAL_KEY) {
logMsg("🚀 Dispatch Ride #$rideId → " . count($drivers) . ' drivers.');
}
// 🆕 Ride Offer Cache + Pending Queue (للتعافي من انقطاع الاتصال)
if ($redis && $rideId && !empty($payload)) {
$offerKey = "ride:offer:$rideId";
$offerTtl = 120;
$redis->setex($offerKey, $offerTtl, json_encode($payload));
$offeredSetKey = "ride:offered_drivers:$rideId";
$redis->sadd($offeredSetKey, ...$drivers);
$redis->expire($offeredSetKey, $offerTtl);
}
foreach ($drivers as $driverId) {
if (!isset($connectedDrivers[$driverId])) continue;
if (!isset($connectedDrivers[$driverId])) {
// السائق غير متصل → نخزن في قائمة الانتظار عشان يسلمله أول ما يوصل
if ($redis && $rideId) {
$pendingKey = "driver:pending_orders:$driverId";
$redis->zadd($pendingKey, time(), $rideId);
$redis->expire($pendingKey, 120);
}
logMsg("📦 Driver #$driverId offline — saved to pending queue.");
continue;
}
$io->to('driver_' . $driverId)->emit('new_ride_request', $payload);
$platform = $connectedDrivers[$driverId]['platform'] ?? 'android';
@@ -376,7 +396,18 @@ $io->on('workerStart', function () use ($io, $INTERNAL_KEY) {
if (!$rideId) { $connection->send('Error: Missing ride_id'); return; }
if ($redis) $redis->zrem('geo:rides:waiting', $rideId);
if ($redis) {
$redis->zrem('geo:rides:waiting', $rideId);
// 🆕 Clean pending offers from all drivers' queues
$offeredSetKey = "ride:offered_drivers:$rideId";
$offeredDrivers = $redis->smembers($offeredSetKey);
foreach ($offeredDrivers as $dId) {
$redis->zrem("driver:pending_orders:$dId", $rideId);
}
$redis->del($offeredSetKey);
$redis->del("ride:offer:$rideId");
}
$io->emit('ride_taken', [
'ride_id' => $rideId,
@@ -480,10 +511,63 @@ $io->on('connection', function ($socket) use ($INTERNAL_KEY) {
logMsg("✅ Driver Connected: #$driverId ($platform)");
// 🆕 Deliver any pending orders missed during disconnection
// نستخدم pipeline لتقليل عدد عمليات Redis (خفيف على السيرفر)
$redis = getRedis();
if ($redis) {
$pendingKey = "driver:pending_orders:$driverId";
$pendingRides = $redis->zrevrangebyscore($pendingKey, time(), time() - 120);
if (!empty($pendingRides)) {
$pipe = $redis->pipeline();
foreach ($pendingRides as $pRideId) {
$offerData = $redis->get("ride:offer:$pRideId");
if ($offerData) {
$decoded = json_decode($offerData, true);
if ($decoded) {
$socket->emit('new_ride_request', $decoded);
logMsg("📦 Re-delivered pending Ride #$pRideId to Driver #$driverId");
// نزيل الرحلة من قائمة الانتظار فوراً
// هذا يمنع إعادة الإرسال عند إعادة الاتصال مرة أخرى (شبكة ضعيفة)
$pipe->zrem($pendingKey, $pRideId);
}
} else {
$pipe->zrem($pendingKey, $pRideId);
}
}
$pipe->execute();
}
}
$socket->on('ping_alive', function () {
// Socket.IO handles pong automatically
});
// 🆕 Client requests any pending orders missed during disconnection
$socket->on('get_pending_orders', function () use ($driverId, $socket) {
$redis = getRedis();
if (!$redis) return;
$pendingKey = "driver:pending_orders:$driverId";
$pendingRides = $redis->zrevrangebyscore($pendingKey, time(), time() - 120);
if (empty($pendingRides)) return;
logMsg("📦 get_pending_orders: " . count($pendingRides) . " pending for Driver #$driverId");
$pipe = $redis->pipeline();
foreach ($pendingRides as $pRideId) {
$offerData = $redis->get("ride:offer:$pRideId");
if ($offerData) {
$decoded = json_decode($offerData, true);
if ($decoded) {
$socket->emit('new_ride_request', $decoded);
$pipe->zrem($pendingKey, $pRideId);
}
} else {
$pipe->zrem($pendingKey, $pRideId);
}
}
$pipe->execute();
});
$socket->on('update_location', function ($data)
use ($driverId, $INTERNAL_KEY, &$driverState, &$fwdThrottle, &$eventBuffer)
{
@@ -49,6 +49,10 @@ Future<bool> onStart(ServiceInstance service) async {
socket.onConnect((_) {
print("✅ Background Service: Socket Connected! ID: ${socket?.id}");
// 🆕 طلب أي رحلات انتظار فاتتنا أثناء انقطاع الخدمة
socket.emit('get_pending_orders');
if (service is AndroidServiceInstance) {
flutterLocalNotificationsPlugin.show(
id: notificationId,
@@ -250,6 +250,10 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (sid != null || eid != null) {
isSocketConnected = true;
_startHeartbeat();
// 🆕 طلب أي رحلات انتظار فاتتنا أثناء انقطاع الاتصال
Log.print("📦 Requesting pending orders after reconnect...");
socket!.emit('get_pending_orders');
}
});
});