import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_overlay_window/flutter_overlay_window.dart'; import 'package:get/get.dart'; import 'package:geolocator/geolocator.dart' as geo; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:location/location.dart'; import 'package:battery_plus/battery_plus.dart'; import 'package:permission_handler/permission_handler.dart' as ph; import 'package:sefer_driver/views/home/Captin/orderCaptin/order_request_page.dart'; import 'package:socket_io_client/socket_io_client.dart' as IO; import 'package:sefer_driver/constant/table_names.dart'; import '../../constant/box_name.dart'; import '../../constant/links.dart'; import '../../main.dart'; import '../home/captin/home_captain_controller.dart'; import '../home/captin/map_driver_controller.dart'; import '../home/payment/captain_wallet_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 = const LatLng(0, 0); double heading = 0.0; double speed = 0.0; double totalDistance = 0.0; bool _isReady = false; bool _isPowerSavingMode = false; final List> _trackBuffer = []; LatLng? _lastPosForDistance; LatLng? _lastRecordedRealLoc; DateTime? _lastRecordedTime; LatLng? _lastSqlLoc; double? _lastSpeed; DateTime? _lastSpeedAt; @override Future onInit() async { super.onInit(); 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') { 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(); } 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) { print("📱 Lifecycle: App is in FOREGROUND"); box.write(BoxName.isAppInForeground, true); // إيقاف خدمة الخلفية لأننا في الواجهة الآن BackgroundServiceHelper.stopService(); // التأكد من أن السوكيت متصل، وإذا لا، نعيد الاتصال فوراً if (socket == null || !socket!.connected) { print("🔄 Socket disconnected in background. Reconnecting now..."); initSocket(); } else { // إذا كان متصلاً، ننعش المستمعين فقط للتأكد _setupSocketListeners(); } } else if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) { print("📱 Lifecycle: App is in BACKGROUND"); box.write(BoxName.isAppInForeground, false); // تشغيل خدمة الخلفية لضمان بقاء التطبيق حياً 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) ====== // =================================================================== void initSocket() { String driverId = box.read(BoxName.driverID).toString(); String token = box.read(BoxName.tokenDriver).toString(); String platform = Platform.isIOS ? 'ios' : 'android'; // 1. إذا كان السوكيت موجوداً، فقط تأكد من اتصاله if (socket != null) { if (!socket!.connected) { print("🟡 Socket exists but disconnected. Reconnecting..."); socket!.connect(); } _setupSocketListeners(); // تحديث المستمعين return; } print("🟡 [LocationController] Creating NEW Socket for Driver: $driverId"); // 2. إنشاء الاتصال socket = IO.io( 'https://location.intaleq.xyz', IO.OptionBuilder() .setTransports(['websocket']) .enableAutoConnect() // تفعيل إعادة الاتصال التلقائي .setQuery( {'driver_id': driverId, 'token': token, 'platform': platform}) .setReconnectionAttempts(double.infinity) .setReconnectionDelay(2000) .build()); socket!.connect(); _setupSocketListeners(); } // دالة منفصلة لضمان عدم تكرار المستمعين void _setupSocketListeners() { if (socket == null) return; // تنظيف القديم أولاً socket!.off('connect'); socket!.off('disconnect'); socket!.off('new_ride_request'); socket!.off('ride_cancelled'); socket!.onConnect((_) { print('✅ Socket Connected! ID: ${socket?.id}'); isSocketConnected = true; _startHeartbeat(); }); socket!.onDisconnect((_) { print('❌ Socket Disconnected'); isSocketConnected = false; _stopHeartbeat(); }); // 🔥 الاستماع للطلبات الجديدة socket!.on('new_ride_request', (data) { 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) { print("❌ Error processing socket data: $e"); } } }); }); // 🔥 الاستماع للإلغاء socket!.on('cancel_ride', (data) { 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 { print("📦 Socket Order Received from ($source)"); try { // 1. التحقق من صحة البيانات if (rideData.isEmpty || !rideData.containsKey('16')) { print("❌ Socket Error: Invalid Ride Data."); return; } // 2. تجهيز البيانات (DriverList) List driverList = []; if (rideData.length > 0) { var sortedKeys = rideData.keys.map((e) => int.tryParse(e) ?? 0).toList() ..sort(); for (var key in sortedKeys) { driverList.add(rideData[key.toString()]); } } // 3. التنقل (باستخدام الاسم لضمان عمل الشرط) try { if (await FlutterOverlayWindow.isActive()) { print("📲 Closing Overlay because App took control via Socket"); await FlutterOverlayWindow.closeOverlay(); } } catch (e) { print("Overlay check error: $e"); } // ✅ هذا الشرط سيعمل الآن بدقة لأننا سنستخدم toNamed if (Get.currentRoute != '/OrderRequestPage') { print("🚀 Socket: Navigating to OrderRequestPage..."); // 🔥 التعديل هنا: استخدمنا Get.toNamed بدلاً من Get.to // هذا يضمن تطابق الاسم مع ما هو موجود في main.dart Get.toNamed('/OrderRequestPage', arguments: { 'myListString': jsonEncode(driverList), 'DriverList': driverList, 'body': 'New Trip Request via Socket ⚡' }); } else { print("⚠️ User is already on OrderRequestPage. Skipping navigation."); } } catch (e) { print("❌ Socket Navigation Error: $e"); } } void _startHeartbeat() { _socketHeartbeat?.cancel(); _socketHeartbeat = Timer.periodic(const Duration(seconds: 25), (timer) { 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'; // 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, }; // 🔥 CRITICAL FIX: Inject Passenger ID if a ride is active 🔥 if (Get.isRegistered()) { final mapCtrl = Get.find(); // Check if ride is started/active and we have a passenger ID if (mapCtrl.isRideStarted && mapCtrl.passengerId != null) { payload['passenger_id'] = mapCtrl.passengerId; // This triggers the PHP forwarding payload['ride_id'] = mapCtrl.rideId; // Good for debugging } } // Debug print to verify // 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; if (_lastPosForDistance != null) { final d = _calculateDistance(_lastPosForDistance!, pos); if (d > 5.0) totalDistance += d; } _lastPosForDistance = pos; update(); emitLocationToSocket(pos, heading, speed); await _saveBehaviorIfMoved(pos, now, currentSpeed: speed); }, onError: (e) => print('❌ Location Stream Error: $e')); } Future stopLocationUpdates() async { print("🛑 Stopping Location Updates..."); _locSub?.cancel(); _locSub = null; _recordTimer?.cancel(); _uploadBatchTimer?.cancel(); _socketHeartbeat?.cancel(); if (socket != null && socket!.connected) { String driverId = box.read(BoxName.driverID).toString(); socket!.emit('update_location', { 'driver_id': driverId, 'lat': myLocation.latitude, 'lng': myLocation.longitude, 'heading': heading, 'speed': speed * 3.6, 'status': 'close', // Changed to off 'distance': totalDistance }); socket!.disconnect(); } await BackgroundServiceHelper.stopService(); socket = null; isSocketConnected = false; _isReady = false; } // =================================================================== // ====== Batch Logic & Helpers ====== // =================================================================== void _startBatchTimers() { _recordTimer?.cancel(); _uploadBatchTimer?.cancel(); final recDur = _isPowerSavingMode ? recordIntervalPowerSave : recordIntervalNormal; final upDur = _isPowerSavingMode ? uploadBatchIntervalPowerSave : uploadBatchIntervalNormal; _recordTimer = Timer.periodic(recDur, (_) => _recordCurrentLocationToBuffer()); _uploadBatchTimer = Timer.periodic(upDur, (_) => _flushBufferToServer()); } 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; List> batch = List.from(_trackBuffer); _trackBuffer.clear(); final String driverId = (box.read(BoxName.driverID) ?? '').toString(); try { await CRUD().post( link: '${AppLink.locationServer}/add_batch.php', payload: {'driver_id': driverId, 'batch_data': jsonEncode(batch)}, ); } catch (e) { print('❌ Failed to upload batch: $e'); } } 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(); startLocationUpdates(); } }); } 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; try { await sql.insertData({ 'driver_id': (box.read(BoxName.driverID) ?? '').toString(), 'latitude': pos.latitude, 'longitude': pos.longitude, 'acceleration': accel, 'created_at': now.toIso8601String(), 'updated_at': now.toIso8601String(), }, TableName.behavior); _lastSqlLoc = pos; } catch (e) { print('SQLite 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) { 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()) { return await location.getLocation(); } } catch (e) { print('❌ FAILED to get single location: $e'); } return null; } }