import 'dart:async'; import 'dart:convert'; import 'dart:ui'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_background_service/flutter_background_service.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 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 '../../constant/box_name.dart'; import '../../constant/links.dart'; import 'package:trip_overlay_plugin/trip_overlay_plugin.dart'; import '../firebase/local_notification.dart'; const String notificationChannelId = 'driver_service_channel'; const int notificationId = 888; const String notificationIcon = '@mipmap/launcher_icon'; @pragma('vm:entry-point') Future onStart(ServiceInstance service) async { DartPluginRegistrant.ensureInitialized(); final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); await GetStorage.init(); final box = GetStorage(); IO.Socket? socket; String driverId = box.read(BoxName.driverID) ?? ''; String token = box.read(BoxName.tokenDriver) ?? ''; if (driverId.isNotEmpty) { socket = IO.io( AppLink.locationSocketUrl, IO.OptionBuilder() .setTransports(['websocket']) .disableAutoConnect() .setQuery({ 'driver_id': driverId, 'token': token, // ‏نفس سبب location_controller: بلا platform يعتبره السيرفر // ‏أندرويد فلا يرسل إشعار FCM الاحتياطي لكباتن الآيفون. 'platform': Platform.isIOS ? 'ios' : 'android', 'EIO': '3', // توافقية مع Workerman }) .setReconnectionAttempts(double.infinity) .build()); socket.connect(); socket.onConnect((_) { print("✅ Background Service: Socket Connected! ID: ${socket?.id}"); // 🆕 طلب أي رحلات انتظار فاتتنا أثناء انقطاع الخدمة socket!.emit('get_pending_orders'); if (service is AndroidServiceInstance) { flutterLocalNotificationsPlugin.show( id: notificationId, title: 'أنت متصل الآن', body: 'بانتظار الطلبات...', notificationDetails: const NotificationDetails( android: AndroidNotificationDetails( notificationChannelId, 'خدمة السائق', icon: notificationIcon, ongoing: true, importance: Importance.low, priority: Priority.low, ), ), ); } }); socket.on('new_ride_request', (data) async { print("🔔 Background Service: Received new_ride_request"); // 🔥 قراءة حالة التطبيق مباشرة قبل العرض await GetStorage.init(); // تأكد من تحديث البيانات final box = GetStorage(); // 🚌 في وضع الباص لا نعرض طلبات الرحلات — السائق في مهمة باص نشطة final bool inBusMode = box.read(BoxName.isBusMode) ?? false; if (inBusMode) { print("🚌 Bus mode active — ignoring new_ride_request in background."); return; } bool isAppInForeground = box.read(BoxName.isAppInForeground) ?? false; // 🔥 Check إضافي: هل الـ Overlay مفتوح بالفعل؟ (للأندرويد فقط) bool overlayActive = false; if (Platform.isAndroid) { overlayActive = await Overlay.FlutterOverlayWindow.isActive(); } if (isAppInForeground || overlayActive) { print("🛑 App is FOREGROUND or Overlay already shown. Skipping."); return; } // عرض الـ Overlay (للأندرويد فقط) if (Platform.isAndroid) { print("🚀 App is BACKGROUND. Showing Overlay..."); try { await Overlay.FlutterOverlayWindow.showOverlay( enableDrag: true, overlayTitle: "طلب جديد", overlayContent: "لديك طلب جديد وصل للتو!", flag: OverlayFlag.focusPointer, positionGravity: PositionGravity.auto, height: WindowSize.matchParent, width: WindowSize.matchParent, startPosition: const OverlayPosition(0, -30), ); await Overlay.FlutterOverlayWindow.shareData(data); } catch (e) { print("Overlay Error: $e"); } } else if (Platform.isIOS) { // على iOS، نظهر إشعاراً عادياً لأن الـ Overlay غير موجود flutterLocalNotificationsPlugin.show( id: 1002, title: "طلب رحلة جديد 🚖", body: "لديك طلب رحلة جديد، افتح التطبيق للموافقة عليه", notificationDetails: const NotificationDetails( iOS: DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ), ), payload: jsonEncode(data)); } }); // ══════════════════════════════════════════════════════════════════ // 🚫 الرحلة أخذها سائق آخر // // ‏نافذة الأوفرلي تعيش في عزلة منفصلة بلا سوكِت خاص بها، فلا تعلم // ‏بشيء من نفسها. وهذه العزلة الخلفية هي الوحيدة التي تسمع الحدث // ‏والتطبيق مغلق. بدون هذا المستمع تبقى النافذة معروضة وعدّادها // ‏يعمل على رحلة لم تعد موجودة، ويضغط السائق "قبول" فيجدها مأخوذة. // // ‏نغلق النافذتين معاً: نافذة فلاتر (مسار السوكِت) والنافذة الأصلية // ‏(مسار FCM) — لا نعرف أيّهما معروضة الآن. // ══════════════════════════════════════════════════════════════════ socket.on('ride_taken', (data) async { try { await GetStorage.init(); final box = GetStorage(); final String myDriverId = box.read(BoxName.driverID).toString(); final String whoTookIt = data?['taken_by_driver_id']?.toString() ?? ''; // ‏لا نغلق على من فاز بها — تطبيقه في طريقه لفتح شاشة الرحلة. if (whoTookIt == myDriverId) return; bool closedAny = false; if (Platform.isAndroid) { if (await Overlay.FlutterOverlayWindow.isActive()) { await Overlay.FlutterOverlayWindow.closeOverlay(); closedAny = true; } if (await TripOverlayPlugin.isOverlayActive()) { await TripOverlayPlugin.hideOverlay(); closedAny = true; } } // ‏الإشعار فقط إن كان هناك ما يُغلق فعلاً — وإلا أزعجنا سائقاً // ‏لم يُعرض عليه شيء أصلاً (الحدث يُبثّ للجميع). if (closedAny) { flutterLocalNotificationsPlugin.show( id: 1003, title: "الطلب لم يعد متاحاً", body: "قام سائق آخر بقبول هذه الرحلة", notificationDetails: const NotificationDetails( iOS: DarwinNotificationDetails( presentAlert: true, presentBadge: false, presentSound: false, ), ), payload: ''); } } catch (e) { print("⚠️ ride_taken handler error: $e"); } }); } // ══════════════════════════════════════════════════════════════════════ // سوكيت الطعام في العزلة الخلفية — مستقل تماماً عن سوكيت الرحلات أعلاه // (خادم وبورت وغرفة مختلفة). بدونه لا يصل عرض توصيل والتطبيق مغلق، لأن // FoodSocketService يعيش في العزلة الرئيسية التي يوقفها أندرويد. // نفتحه فقط إن كان وضع التوصيل مفعّلاً — لا اتصال دائم بلا داعٍ. // ══════════════════════════════════════════════════════════════════════ IO.Socket? foodSocket; void connectFoodSocket() { if (foodSocket != null || driverId.isEmpty) return; foodSocket = IO.io( AppLink.foodSocketUrl, IO.OptionBuilder() .setTransports(['websocket']) .setQuery({ 'role': 'driver', 'id': driverId, 'jwt': token, 'platform': Platform.isIOS ? 'ios' : 'android', }) .enableReconnection() .setReconnectionDelay(3000) .build()); foodSocket!.onConnect((_) => print('🍔 Background: food socket connected')); foodSocket!.on('food_delivery_offer', (data) async { print('🍔 Background: food delivery offer received'); await GetStorage.init(); final box = GetStorage(); // الواجهة أمام السائق تتكفّل بالعرض إن كان التطبيق مفتوحاً final bool isAppInForeground = box.read(BoxName.isAppInForeground) ?? false; if (isAppInForeground) return; Map? payload; if (data is Map) { payload = Map.from(data); } else if (data is List && data.isNotEmpty && data.first is Map) { payload = Map.from(data.first as Map); } if (payload == null) return; if (Platform.isAndroid) { final shown = await TripOverlayPlugin.showFoodOverlay( FoodOrderOverlayData( orderId: payload['order_id'].toString(), merchantName: payload['merchant_name_ar']?.toString() ?? 'مطعم', merchantAddress: payload['merchant_address']?.toString() ?? '', deliveryFeeText: _formatFoodPrice(payload['delivery_fee']), cashToCollectText: _formatFoodPrice(payload['cash_to_collect']), distanceText: '', itemsCount: int.tryParse(payload['items_count']?.toString() ?? '') ?? 0, isCash: payload['payment_method']?.toString() == 'cash', ), autoCloseSeconds: 20, ); if (shown) return; } // iOS أو تعذّرت النافذة — إشعار على قناة التوصيل الخاصة flutterLocalNotificationsPlugin.show( id: 7001, title: '🍔 عرض توصيل جديد', body: '${payload['merchant_name_ar'] ?? 'مطعم'} — ' '${_formatFoodPrice(payload['delivery_fee'])}', notificationDetails: const NotificationDetails( android: AndroidNotificationDetails( 'food_delivery_offer_channel', 'عروض التوصيل', importance: Importance.max, priority: Priority.max, fullScreenIntent: true, category: AndroidNotificationCategory.call, ), iOS: DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ), ), payload: jsonEncode({'food_event': 'offer', 'order_id': payload['order_id']}), ); }); } if (box.read(BoxName.foodDeliveryModeEnabled) == true) { connectFoodSocket(); } // تفعيل/إطفاء وضع التوصيل من الواجهة ينعكس هنا بلا إعادة تشغيل الخدمة service.on('foodModeChanged').listen((event) { final enabled = event?['enabled'] == true; if (enabled) { connectFoodSocket(); } else { foodSocket?.clearListeners(); foodSocket?.dispose(); foodSocket = null; } }); service.on('stopService').listen((event) { socket?.clearListeners(); socket?.dispose(); foodSocket?.clearListeners(); foodSocket?.dispose(); service.stopSelf(); }); // 🚫 [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) { if (await service.isForegroundService()) { flutterLocalNotificationsPlugin.show( id: notificationId, title: 'خدمة السائق نشطة', body: 'بانتظار الطلبات...', notificationDetails: const NotificationDetails( android: AndroidNotificationDetails( notificationChannelId, 'خدمة السائق', icon: notificationIcon, ongoing: true, importance: Importance.low, priority: Priority.low, ), ), ); } } }); return true; } // نفس قاعدة foodFormatPrice في نماذج الطعام — مكرّرة هنا عمداً لأن العزلة // الخلفية يجب أن تبقى خفيفة الاعتماديات ولا تسحب طبقة النماذج كاملة. String _formatFoodPrice(dynamic smallestUnit) { final v = int.tryParse(smallestUnit?.toString() ?? '') ?? 0; if (v == 0) return ''; return '${(v / 1000).toStringAsFixed(3)} د.أ'; } class BackgroundServiceHelper { static Future initialize() async { final service = FlutterBackgroundService(); await service.configure( androidConfiguration: AndroidConfiguration( onStart: onStart, autoStart: false, isForegroundMode: true, notificationChannelId: notificationChannelId, initialNotificationTitle: 'تطبيق السائق', initialNotificationContent: 'تجهيز الخدمة...', foregroundServiceNotificationId: notificationId, ), iosConfiguration: IosConfiguration( autoStart: false, onForeground: onStart, onBackground: onStart, ), ); } static Future startService() async { final service = FlutterBackgroundService(); if (!await service.isRunning()) { await service.startService(); } } static Future stopService() async { final service = FlutterBackgroundService(); service.invoke("stopService"); } }