import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:geolocator/geolocator.dart' as geo; import 'package:intaleq_maps/intaleq_maps.dart'; import 'package:location/location.dart'; import 'package:battery_plus/battery_plus.dart'; import 'package:permission_handler/permission_handler.dart' as ph; import 'package:socket_io_client/socket_io_client.dart' as IO; import 'package:siro_driver/constant/table_names.dart'; import 'package:trip_overlay_plugin/trip_overlay_plugin.dart'; import '../../constant/box_name.dart'; import '../../constant/links.dart'; import '../../main.dart'; import '../../print.dart'; 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'; class LocationController extends GetxController with WidgetsBindingObserver { // =================================================================== // ====== Tunables ====== // =================================================================== static const Duration recordIntervalNormal = Duration(seconds: 3); static const Duration uploadBatchIntervalNormal = Duration(minutes: 2); static const Duration recordIntervalPowerSave = Duration(seconds: 10); static const Duration uploadBatchIntervalPowerSave = Duration(minutes: 5); static const double lowWalletThreshold = -200; static const int powerSaveTriggerLevel = 20; static const int powerSaveExitLevel = 25; // =================================================================== // ====== Services & Variables ====== // =================================================================== late final Location location = Location(); final Battery _battery = Battery(); IO.Socket? socket; bool isSocketConnected = false; Timer? _socketHeartbeat; StreamSubscription? _locSub; StreamSubscription? _batterySub; Timer? _recordTimer; Timer? _uploadBatchTimer; late final HomeCaptainController _homeCtrl; late final CaptainWalletController _walletCtrl; LatLng myLocation = LatLng( box.read('last_lat') ?? 0.0, box.read('last_lng') ?? 0.0, ); double heading = box.read('last_heading') ?? 0.0; double speed = 0.0; double totalDistance = 0.0; bool _isReady = false; bool _isPowerSavingMode = false; final List> _trackBuffer = []; final List> _behaviorBuffer = []; LatLng? _lastPosForDistance; LatLng? _lastRecordedRealLoc; DateTime? _lastRecordedTime; LatLng? _lastSqlLoc; double? _lastSpeed; DateTime? _lastSpeedAt; @override Future onInit() async { super.onInit(); Log.print('🚀 LocationController Starting...'); // 1. Register Lifecycle Observer WidgetsBinding.instance.addObserver(this); box.write(BoxName.isAppInForeground, true); // مراقب الحالة (Status Watcher) box.listenKey(BoxName.statusDriverLocation, (value) { if (value == 'blocked') { Log.print("⛔ Driver is Blocked: Force Stopping Location Updates."); stopLocationUpdates(); if (socket != null && socket!.connected) { socket!.emit('update_location', { 'driver_id': box.read(BoxName.driverID), 'status': 'blocked', 'lat': myLocation.latitude, 'lng': myLocation.longitude, 'heading': heading, 'speed': speed * 3.6, 'distance': totalDistance }); socket!.disconnect(); } } }); bool deps = await _awaitDependencies(); if (!deps) return; _isReady = true; initSocket(); await _initLocationSettings(); _listenToBatteryChanges(); if (box.read(BoxName.statusDriverLocation) != 'blocked') { await startLocationUpdates(); } Log.print('✅ LocationController Initialized.'); } @override void onClose() { WidgetsBinding.instance.removeObserver(this); box.write(BoxName.isAppInForeground, false); stopLocationUpdates(); _batterySub?.cancel(); _stopHeartbeat(); socket?.dispose(); super.onClose(); } // =================================================================== // 🔥 Lifecycle Manager (Fixes Freeze & Background issues) // =================================================================== @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { Log.print("📱 Lifecycle: App is in FOREGROUND"); box.write(BoxName.isAppInForeground, true); // إيقاف خدمة الخلفية BackgroundServiceHelper.stopService(); if (socket == null || (!socket!.connected && !_isInitializingSocket)) { Log.print("🔄 Initializing Socket on resume..."); initSocket(); } } else if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) { Log.print("📱 Lifecycle: App is in BACKGROUND"); box.write(BoxName.isAppInForeground, false); // تشغيل خدمة الخلفية للأندرويد لضمان بقاء التطبيق حياً if (!Platform.isIOS) { BackgroundServiceHelper.startService(); } } } Future _awaitDependencies() async { int attempts = 0; while (attempts < 10) { if (Get.isRegistered() && Get.isRegistered()) { _homeCtrl = Get.find(); _walletCtrl = Get.find(); return true; } await Future.delayed(const Duration(milliseconds: 500)); attempts++; } return false; } // =================================================================== // ====== Socket Logic (Improved) ====== // =================================================================== bool _isInitializingSocket = false; void initSocket() { // منع الاستدعاءات المتداخلة التي تسبب قتل الاتصال قبل اكتماله if (_isInitializingSocket) { Log.print("⏳ Socket is already initializing. Skipping redundant call."); return; } if (socket != null && socket!.connected) { Log.print("✅ Socket is already connected. No need to re-init."); return; } String driverId = box.read(BoxName.driverID).toString(); String token = box.read(BoxName.tokenDriver).toString(); String platform = Platform.isIOS ? 'ios' : 'android'; _isInitializingSocket = true; // تنظيف السوكيت القديم فقط إذا كان موجوداً وغير متصل if (socket != null) { Log.print("🧹 Cleaning up old socket instance..."); socket!.clearListeners(); socket!.dispose(); socket = null; } Log.print( "🟡 [LocationController] Initializing NEW Socket for Driver: $driverId"); try { // العودة للـ Websocket حصراً لأنه الوحيد الذي ينجح في فتح القناة socket = IO.io( AppLink.locationSocketUrl, IO.OptionBuilder() .setTransports(['websocket']) .setQuery({'driver_id': driverId, 'token': token, 'EIO': '3'}) .enableForceNew() .build()); _setupSocketListeners(); socket!.connect(); } catch (e) { _isInitializingSocket = false; Log.print("❌ Socket Initialization Exception: $e"); } } void _setupSocketListeners() { if (socket == null) return; socket!.off('connect'); socket!.off('disconnect'); socket!.off('connect_error'); socket!.off('error'); socket!.onConnect((_) { _isInitializingSocket = false; // ننتظر قليلاً للتأكد من تعبئة الـ IDs Future.delayed(const Duration(milliseconds: 1000), () { String? sid = socket?.id; String? eid = socket?.io.engine?.id; Log.print( '✅ Socket Connected! ID: ${sid ?? eid ?? 'N/A'} (SID: $sid, EID: $eid)'); if (sid != null || eid != null) { isSocketConnected = true; _startHeartbeat(); } }); }); socket!.onDisconnect((data) { _isInitializingSocket = false; Log.print('❌ Socket Disconnected: $data'); isSocketConnected = false; _stopHeartbeat(); }); socket!.onConnectError((err) { _isInitializingSocket = false; Log.print('❌ Socket Connect Error: $err'); }); socket!.onConnectTimeout((data) { _isInitializingSocket = false; Log.print('❌ Socket Connect Timeout: $data'); }); socket!.onError((err) { _isInitializingSocket = false; Log.print('❌ Socket General Error: $err'); }); socket!.on('reconnect_attempt', (attempt) { Log.print('🔄 Socket Reconnecting... Attempt: $attempt'); }); // 🔥 الاستماع للطلبات الجديدة socket!.on('new_ride_request', (data) { Log.print("🔔 Socket: New Ride Request Arrived!"); // نستخدم Future.microtask لضمان عدم حظر الـ UI Thread Future.microtask(() { if (data != null) { try { List rawList = []; if (data is String) { var decoded = jsonDecode(data); if (decoded is List) rawList = decoded; } else if (data is List) { if (data.isNotEmpty) { rawList = (data[0] is List) ? data[0] : data; } } if (rawList.isNotEmpty) { Map convertedMap = {}; for (int i = 0; i < rawList.length; i++) { convertedMap[i.toString()] = rawList[i]; } handleIncomingOrder(convertedMap, "Socket"); } } catch (e) { Log.print("❌ Error processing socket data: $e"); } } }); }); // 🔥 الاستماع للإلغاء socket!.on('cancel_ride', (data) { Log.print("🚫 Socket: Ride Cancelled Event Received"); String reason = data['reason'] ?? 'No reason provided'; if (Get.isRegistered()) { Get.find() .processRideCancelledByPassenger(reason, source: "Socket"); } }); } // داخل LocationController Future handleIncomingOrder( Map rideData, String source) async { Log.print("📦 Socket Order Received from ($source)"); // 🔴 1. التحقق من حالة التطبيق قبل أي شيء 🔴 bool isAppInForeground = box.read(BoxName.isAppInForeground) ?? false; if (!isAppInForeground) { Log.print( "📱 [LocationController] Order received in background (iOS/Android). Source: $source"); if (Platform.isIOS) { // على iOS، نقوم بإظهار إشعار محلي لأن الـ Overlay غير مدعوم NotificationController().showNotification( "طلب رحلة جديد 🚖", "لديك طلب رحلة جديد، افتح التطبيق للموافقة عليه", jsonEncode(rideData), 'ding.wav'); } return; } try { // 2. التحقق من صحة البيانات if (rideData.isEmpty || !rideData.containsKey('16')) { Log.print("❌ Socket Error: Invalid Ride Data."); return; } // 3. تجهيز البيانات (DriverList) List driverList = []; if (rideData.isNotEmpty) { var sortedKeys = rideData.keys .where((e) => int.tryParse(e) != null) .map((e) => int.parse(e)) .toList() ..sort(); for (var key in sortedKeys) { driverList.add(rideData[key.toString()]); } } // الحماية ضد البنية غير المكتملة if (driverList.length <= 16) { Log.print("❌ Socket Error: Parsed driver list is incomplete."); return; } // 4. إغلاق النافذة (إن وجدت بالخطأ) والتنقل try { if (await TripOverlayPlugin.isOverlayActive()) { Log.print("📲 Closing Overlay because App took control via Socket"); await TripOverlayPlugin.hideOverlay(); } } catch (e) { Log.print("Overlay check error: $e"); } // 🔥 [Fix Active-Ride Guard] منع فتح صفحة الطلبات أثناء وجود السائق في رحلة نشطة // هذا يمنع socket event جديد من تعطيل رحلة جارية String? currentRideStatus = box.read(BoxName.rideStatus); bool hasActiveRide = (currentRideStatus == 'Begin' || currentRideStatus == 'Apply' || currentRideStatus == 'Arrived'); String currentRoute = Get.currentRoute; bool isOnMapPage = currentRoute.contains('MapPage') || currentRoute.contains('PassengerLocation'); if (hasActiveRide || isOnMapPage) { Log.print( "⛔ [LocationController] Ignoring new ride request — driver has active ride ($currentRideStatus) or is on map page ($currentRoute)."); return; } if (currentRoute != '/OrderRequestPage') { Log.print("🚀 Socket: Navigating to OrderRequestPage..."); Get.toNamed('/OrderRequestPage', arguments: { 'myListString': jsonEncode(driverList), 'DriverList': driverList, 'body': 'New Trip Request via Socket ⚡' }); } else { Log.print( "⚠️ User is already on OrderRequestPage. Skipping navigation."); } } catch (e) { Log.print("❌ Socket Navigation Error: $e"); } } void _startHeartbeat() { _socketHeartbeat?.cancel(); _socketHeartbeat = Timer.periodic(const Duration(seconds: 25), (timer) { // [Fix 6] تخطي الإرسال إذا كان stream الموقع نشطاً. // الـ _locSub يرسل update_location عند كل تحرك (كل 5-10 ثوانٍ) تلقائياً. // الـ heartbeat يكون مفيداً فقط عندما يتوقف الـ stream (الجهاز ثابت أو أوقف الخدمة). if (_locSub != null) return; if (socket != null && isSocketConnected && myLocation.latitude != 0) { emitLocationToSocket(myLocation, heading, speed); } }); } void _stopHeartbeat() { _socketHeartbeat?.cancel(); } // In LocationController.dart void emitLocationToSocket(LatLng pos, double head, double spd) { String status = box.read(BoxName.statusDriverLocation) ?? 'on'; String? currentRideStatus = box.read(BoxName.rideStatus); String? storedPassengerId = box.read(BoxName.passengerID); String? storedRideId = box.read(BoxName.rideId); // Basic payload var payload = { 'driver_id': box.read(BoxName.driverID), 'lat': pos.latitude, 'lng': pos.longitude, 'heading': head, 'speed': spd * 3.6, 'status': status, 'distance': totalDistance, }; // 🔥 القرار الذكي: حقن بيانات الراكب إذا كان هناك رحلة نشطة في الـ Box 🔥 bool hasActiveRide = (currentRideStatus == 'Begin' || currentRideStatus == 'Apply' || currentRideStatus == 'Arrived'); if (hasActiveRide && storedPassengerId != null) { payload['passenger_id'] = storedPassengerId; payload['ride_id'] = storedRideId; } // DebugLog.print to verify //Log.print('🚀 Emitting Location: $payload'); if (socket != null && socket!.connected) { socket!.emit('update_location', payload); } } // =================================================================== // ====== Tracking Logic ====== // =================================================================== Future startLocationUpdates() async { _isReady = true; String currentStatus = box.read(BoxName.statusDriverLocation) ?? 'off'; if (currentStatus == 'blocked') { stopLocationUpdates(); return; } // Start background service await BackgroundServiceHelper.startService(); if (socket == null || !socket!.connected) { initSocket(); } if (_locSub != null) return; if (await _ensureServiceAndPermission()) { _subscribeLocationStream(); _startBatchTimers(); } } Future _subscribeLocationStream() async { _locSub?.cancel(); int interval = _isPowerSavingMode ? 10000 : 5000; await location.enableBackgroundMode(enable: true); location.changeSettings( accuracy: LocationAccuracy.navigation, interval: interval, distanceFilter: _isPowerSavingMode ? 20 : 10, ); _locSub = location.onLocationChanged.listen((LocationData loc) async { if (loc.latitude == null || loc.longitude == null) return; final now = DateTime.now(); final pos = LatLng(loc.latitude!, loc.longitude!); myLocation = pos; speed = loc.speed ?? 0.0; heading = loc.heading ?? 0.0; box.write('last_lat', pos.latitude); box.write('last_lng', pos.longitude); box.write('last_heading', heading); if (_lastPosForDistance != null) { final d = _calculateDistance(_lastPosForDistance!, pos); if (d > 5.0) totalDistance += d; } _lastPosForDistance = pos; update(); emitLocationToSocket(pos, heading, speed); if (Get.isRegistered()) { final homeCtrl = Get.find(); if (homeCtrl.isActive && homeCtrl.mapHomeCaptainController != null && homeCtrl.isHomeMapActive && homeCtrl.isMapReadyForCommands) { homeCtrl.mapHomeCaptainController?.animateCamera( CameraUpdate.newLatLngZoom(pos, 17.5), ); } } if (Get.isRegistered()) { final mapCtrl = Get.find(); mapCtrl.handleLocationUpdateFromCentral(pos, speed, heading); } if (Get.isRegistered()) { final navCtrl = Get.find(); navCtrl.handleLocationUpdateFromCentral(pos, speed, heading); } await _saveBehaviorIfMoved(pos, now, currentSpeed: speed); }, onError: (e) => Log.print('❌ Location Stream Error: $e')); } Timer? _socketWatchdogTimer; Future stopLocationUpdates() async { Log.print("🛑 Stopping Location Updates..."); _locSub?.cancel(); _locSub = null; _recordTimer?.cancel(); _uploadBatchTimer?.cancel(); _socketHeartbeat?.cancel(); _socketWatchdogTimer?.cancel(); if (socket != null) { socket!.clearListeners(); socket!.dispose(); } if (!Platform.isIOS) { await BackgroundServiceHelper.stopService(); } socket = null; isSocketConnected = false; _isReady = false; } // =================================================================== // ====== Batch Logic & Helpers ====== // =================================================================== void _startBatchTimers() { _recordTimer?.cancel(); _uploadBatchTimer?.cancel(); _socketWatchdogTimer?.cancel(); final recDur = _isPowerSavingMode ? recordIntervalPowerSave : recordIntervalNormal; final upDur = _isPowerSavingMode ? uploadBatchIntervalPowerSave : uploadBatchIntervalNormal; _recordTimer = Timer.periodic(recDur, (_) => _recordCurrentLocationToBuffer()); _uploadBatchTimer = Timer.periodic(upDur, (_) => _flushBufferToServer()); // محاولة إعادة الاتصال بالسوكيت إذا انقطع كل 3 ثواني _socketWatchdogTimer = Timer.periodic(const Duration(seconds: 3), (_) { if (!isSocketConnected && !_isInitializingSocket) { Log.print("🔄 Socket Watchdog: Attempting to reconnect socket..."); initSocket(); } }); } void _recordCurrentLocationToBuffer() { if (myLocation.latitude == 0) return; final now = DateTime.now(); double distFromLast = 0.0; if (_lastRecordedRealLoc != null) { distFromLast = _calculateDistance(_lastRecordedRealLoc!, myLocation); } bool moved = distFromLast > 10.0; bool timeForced = _lastRecordedTime == null || now.difference(_lastRecordedTime!).inSeconds >= 60; if ((moved && speed > 0.5) || timeForced) { _lastRecordedRealLoc = myLocation; _lastRecordedTime = now; final point = { 'lat': double.parse(myLocation.latitude.toStringAsFixed(6)), 'lng': double.parse(myLocation.longitude.toStringAsFixed(6)), 'spd': double.parse((speed * 3.6).toStringAsFixed(1)), 'head': int.parse(heading.toStringAsFixed(0)), 'st': box.read(BoxName.statusDriverLocation) ?? 'off', 'ts': now.toIso8601String(), }; _trackBuffer.add(point); } } Future _flushBufferToServer() async { if (_trackBuffer.isEmpty) return; int itemsToTake = _trackBuffer.length > 100 ? 100 : _trackBuffer.length; List> batch = _trackBuffer.sublist(0, itemsToTake); final String driverId = (box.read(BoxName.driverID) ?? '').toString(); try { var res = await CRUD().post( link: '${AppLink.locationServer}/add_batch.php', payload: {'driver_id': driverId, 'batch_data': jsonEncode(batch)}, ); if (res != 'failure') { _trackBuffer.removeRange(0, itemsToTake); } else { _enforceBufferLimit(); } } catch (e) { Log.print('❌ Failed to upload batch: $e'); _enforceBufferLimit(); } } void _enforceBufferLimit() { if (_trackBuffer.length > 500) { _trackBuffer.removeRange(0, _trackBuffer.length - 500); Log.print("⚠️ Buffer limit enforced. Removed oldest entries."); } } void _listenToBatteryChanges() async { _battery.onBatteryStateChanged.listen((state) async { int level = await _battery.batteryLevel; bool previousMode = _isPowerSavingMode; if (level <= powerSaveTriggerLevel) _isPowerSavingMode = true; if (level >= powerSaveExitLevel) _isPowerSavingMode = false; if (previousMode != _isPowerSavingMode) { _startBatchTimers(); _updateLocationSettings(); } }); } Future _updateLocationSettings() async { if (_locSub == null) return; int interval = _isPowerSavingMode ? 10000 : 5000; try { await location.changeSettings( accuracy: LocationAccuracy.navigation, interval: interval, distanceFilter: _isPowerSavingMode ? 20 : 10, ); Log.print( "🔋 Location settings updated. Power Save: $_isPowerSavingMode"); } catch (e) { Log.print("❌ Failed to update location settings: $e"); } } Future _saveBehaviorIfMoved(LatLng pos, DateTime now, {required double currentSpeed}) async { final dist = (_lastSqlLoc == null) ? 999.0 : _calculateDistance(_lastSqlLoc!, pos); if (dist < 15.0) return; final accel = _calcAcceleration(currentSpeed, now) ?? 0.0; _lastSqlLoc = pos; _behaviorBuffer.add({ 'driver_id': (box.read(BoxName.driverID) ?? '').toString(), 'latitude': pos.latitude, 'longitude': pos.longitude, 'acceleration': accel, 'created_at': now.toIso8601String(), 'updated_at': now.toIso8601String(), }); if (_behaviorBuffer.length >= 10) { _flushBehaviorBuffer(); } } void _flushBehaviorBuffer() { if (_behaviorBuffer.isEmpty) return; List> batch = List.from(_behaviorBuffer); _behaviorBuffer.clear(); Future.microtask(() async { try { for (var data in batch) { await sql.insertData(data, TableName.behavior); } } catch (e) { Log.print('SQLite Batch Insert Error: $e'); } }); } // استبدال دالة Haversine اليدوية بـ Geolocator في باقي الكود أيضاً // لأنها تعتمد على C++ في الأندرويد و Obj-C في الآيفون (Native Speed) double _calculateDistance(LatLng a, LatLng b) { return geo.Geolocator.distanceBetween( a.latitude, a.longitude, b.latitude, b.longitude); } double? _calcAcceleration(double currentSpeed, DateTime now) { if (_lastSpeed != null && _lastSpeedAt != null) { final dt = now.difference(_lastSpeedAt!).inMilliseconds / 1000.0; if (dt > 0.5) { final a = (currentSpeed - _lastSpeed!) / dt; _lastSpeed = currentSpeed; _lastSpeedAt = now; return a; } } _lastSpeed = currentSpeed; _lastSpeedAt = now; return null; } Future _initLocationSettings() async { if (await _ensureServiceAndPermission()) { try { await location.enableBackgroundMode(enable: true); location.changeSettings( accuracy: LocationAccuracy.navigation, interval: 1000, distanceFilter: 10); } catch (e) { Log.print("Warning: $e"); } } } // 🔥🔥 هذه هي الدالة المعدلة التي تستخدم ph.Permission 🔥🔥 Future _ensureServiceAndPermission() async { // 1. طلب إذن الإشعارات أولاً باستخدام permission_handler if (Platform.isAndroid) { var notificationStatus = await ph.Permission.notification.status; if (!notificationStatus.isGranted) { await ph.Permission.notification.request(); } } // 2. طلب تفعيل خدمة الموقع (GPS) من بكج location bool serviceEnabled = await location.serviceEnabled(); if (!serviceEnabled) { serviceEnabled = await location.requestService(); if (!serviceEnabled) return false; } // 3. طلب إذن الموقع الأساسي من بكج location PermissionStatus permissionGranted = await location.hasPermission(); if (permissionGranted == PermissionStatus.denied) { permissionGranted = await location.requestPermission(); if (permissionGranted != PermissionStatus.granted) return false; } return true; } // ... (باقي الكود) Future getLocation() async { try { if (await _ensureServiceAndPermission()) { final locData = await location.getLocation(); if (locData != null && locData.latitude != null && locData.longitude != null) { myLocation = LatLng(locData.latitude!, locData.longitude!); heading = locData.heading ?? 0.0; speed = locData.speed ?? 0.0; box.write('last_lat', myLocation.latitude); box.write('last_lng', myLocation.longitude); box.write('last_heading', heading); update(); if (Get.isRegistered()) { final homeCtrl = Get.find(); if (homeCtrl.mapHomeCaptainController != null && homeCtrl.isMapReadyForCommands) { Log.print( "📍 [LocationController] Animating camera to single location update"); homeCtrl.mapHomeCaptainController?.animateCamera( CameraUpdate.newLatLngZoom(myLocation, 17.5), ); } } } return locData; } } catch (e) { Log.print('❌ FAILED to get single location: $e'); } return null; } }