import 'dart:async'; import 'dart:convert'; import 'package:get/get.dart'; import 'package:socket_io_client/socket_io_client.dart' as io_client; import 'package:intaleq_maps/intaleq_maps.dart'; import '../../../constant/box_name.dart'; import '../../../constant/links.dart'; import '../../../main.dart'; // contains global 'box' import '../../../print.dart'; import 'ride_lifecycle_controller.dart'; import 'nearby_drivers_controller.dart'; import 'map_engine_controller.dart'; class MapSocketController extends GetxController { late io_client.Socket socket; bool isSocketConnected = false; bool _isSocketInitialized = false; Timer? _heartbeatTimer; DateTime? _lastSocketLocationTime; int _socketLocationUpdatesCount = 0; Timer? _watchdogTimer; DateTime? get lastDriverLocationTime => _lastSocketLocationTime; int get socketLocationUpdatesCount => _socketLocationUpdatesCount; void initConnectionWithSocket() { if (isSocketConnected) return; String passengerId = box.read(BoxName.passengerID).toString(); Log.print("🔌 Initializing Socket for Passenger: $passengerId"); socket = io_client.io( AppLink.serverSocket, io_client.OptionBuilder() .setTransports(['websocket']) .disableAutoConnect() .setQuery({'id': passengerId}) .setReconnectionAttempts(20) .setReconnectionDelay(2000) .setReconnectionDelayMax(10000) .enableReconnection() .setTimeout(20000) .setExtraHeaders({'Connection': 'Upgrade'}) .build(), ); _isSocketInitialized = true; socket.connect(); socket.onConnect((_) { Log.print("✅ Socket Connected Successfully"); isSocketConnected = true; _startHeartbeat(); final rideLifecycle = Get.find(); if (rideLifecycle.rideId != 'yet' && rideLifecycle.driverId.isNotEmpty) { socket.emit('subscribe_driver_location', { 'ride_id': rideLifecycle.rideId, 'driver_id': rideLifecycle.driverId, }); Log.print("📡 Re-subscribed to driver location after connect"); } update(); }); socket.onDisconnect((_) { Log.print("⚠️ Socket Disconnected — Auto-Reconnect will handle it"); isSocketConnected = false; final rideLifecycle = Get.find(); if (rideLifecycle.isActiveRideState()) { Log.print("🔄 Enabling Fast Polling Fallback (4s) until reconnect..."); rideLifecycle.startMasterTimerWithInterval(4); } update(); }); socket.onReconnect((_) { Log.print("🔁 Socket Reconnected Successfully!"); isSocketConnected = true; _startHeartbeat(); final rideLifecycle = Get.find(); if (rideLifecycle.rideId != 'yet' && rideLifecycle.driverId.isNotEmpty) { socket.emit('subscribe_driver_location', { 'ride_id': rideLifecycle.rideId, 'driver_id': rideLifecycle.driverId, }); Log.print("📡 Re-subscribed to driver location after reconnect"); } if (rideLifecycle.isActiveRideState()) { Log.print("✅ Socket back online — stopping Fast Polling Fallback"); rideLifecycle.cancelMasterTimer(); } update(); }); socket.onReconnectAttempt((attemptNumber) { Log.print("🔄 Socket Reconnect Attempt #$attemptNumber..."); }); socket.onError((error) { Log.print("❌ Socket Error: $error"); isSocketConnected = false; }); socket.on('connect_error', (error) { Log.print("❌ Socket Connect Error: $error"); isSocketConnected = false; // في الإصدار 1.0.2 أحياناً auto-reconnect لا يعمل بعد connect_error // نتأكد يدوياً من إعادة الاتصال Future.delayed(const Duration(seconds: 3), () { if (!isSocketConnected && _isSocketInitialized) { Log.print("🔄 Manual reconnect after connect_error..."); try { socket.connect(); } catch (e) { Log.print("Manual reconnect error: $e"); } } }); }); socket.on('ride_status_change', (data) { Log.print("📩 Socket Event: ride_status_change -> $data"); _handleRideStatusChangeWithSocket(data); }); socket.on('driver_location_update', (data) { handleDriverLocationUpdate(data); }); } void _startHeartbeat() { _heartbeatTimer?.cancel(); _heartbeatTimer = Timer.periodic(const Duration(seconds: 15), (timer) { if (isSocketConnected && socket.connected) { socket.emit('heartbeat', {'passenger_id': box.read(BoxName.passengerID).toString()}); } }); } bool isSocketHealthy() { if (!isSocketConnected) return false; if (_lastSocketLocationTime == null) return false; final diff = DateTime.now().difference(_lastSocketLocationTime!).inSeconds; return diff < 20; } void _handleRideStatusChangeWithSocket(dynamic data) { if (data == null || data['status'] == null) return; String newStatus = data['status'].toString().toLowerCase(); Log.print("🔔 Socket Status Update: $newStatus"); final rideLifecycle = Get.find(); Map? driverInfo; if (data['driver_info'] != null && data['driver_info'] is Map) { driverInfo = Map.from(data['driver_info']); } switch (newStatus) { case 'accepted': case 'apply': case 'applied': rideLifecycle.processRideAcceptance( driverData: driverInfo, source: "Socket"); break; case 'arrived': rideLifecycle.processDriverArrival("Socket"); break; case 'started': case 'begin': rideLifecycle.processRideBegin(source: "Socket"); break; case 'finished': case 'ended': _onRideFinishedWithSocket(data); break; case 'cancelled': rideLifecycle.processRideCancelledByDriver(data, source: "Socket"); break; case 'no_drivers_found': rideLifecycle.showNoDriverDialog(); break; } } void _onRideFinishedWithSocket(dynamic data) { Log.print("🏁 Ride Finished (Socket)"); final rideLifecycle = Get.find(); var rawList = data['DriverList']; List listToSend = []; if (rawList != null) { if (rawList is List) { listToSend = rawList; } else if (rawList is String) { try { listToSend = jsonDecode(rawList); } catch (e) { Log.print("Error decoding DriverList: $e"); } } } if (listToSend.isEmpty && data['price'] != null) { listToSend = [ rideLifecycle.driverId, rideLifecycle.rideId, rideLifecycle.driverToken, data['price'].toString() ]; } rideLifecycle.processRideFinished(listToSend, source: "Socket"); } void handleDriverLocationUpdate(dynamic data) { if (!isSocketConnected || data == null) return; _lastSocketLocationTime = DateTime.now(); _socketLocationUpdatesCount++; final rideLifecycle = Get.find(); if (rideLifecycle.driverId.isEmpty && (data['driver_id'] ?? data['driverId']) != null) { rideLifecycle.driverId = (data['driver_id'] ?? data['driverId']).toString(); } if (_socketLocationUpdatesCount >= 3 && rideLifecycle.locationPollingTimer != null) { Log.print("✅ Socket delivering locations reliably. Stopping polling."); rideLifecycle.stopDriverLocationPolling(); } try { double lat = double.tryParse( (data['latitude'] ?? data['lat'])?.toString() ?? '0') ?? 0; double lng = double.tryParse( (data['longitude'] ?? data['lng'])?.toString() ?? '0') ?? 0; double heading = double.tryParse(data['heading']?.toString() ?? '0') ?? 0; if (lat == 0 || lng == 0) return; LatLng newPos = LatLng(lat, lng); final nearbyDrivers = Get.find(); if (nearbyDrivers.driverCarsLocationToPassengerAfterApplied.isEmpty) { nearbyDrivers.driverCarsLocationToPassengerAfterApplied.add(newPos); } else { nearbyDrivers.driverCarsLocationToPassengerAfterApplied[0] = newPos; } double speed = double.tryParse(data['speed']?.toString() ?? '0') ?? 0; rideLifecycle.checkAndRecalculateIfDeviated( newPos, heading: heading, speed: speed, ); final mapEngine = Get.find(); if (mapEngine.mapController != null) { double zoom = 16.5; if (speed > 0) { zoom = 17.0 - ((speed - 10) / 70) * 2.5; zoom = zoom.clamp(14.5, 17.0); } mapEngine.mapController! .animateCamera(CameraUpdate.newLatLngZoom(newPos, zoom)); } final dynamic distanceValue = data['distance_m'] ?? data['distance_meters'] ?? data['distance']; final double? distanceMeters = double.tryParse(distanceValue?.toString() ?? ''); final int? etaSeconds = data['eta_seconds'] == null ? null : int.tryParse(data['eta_seconds'].toString()); final bool hasServerMetrics = (etaSeconds != null && etaSeconds > 0) || (distanceMeters != null && distanceMeters > 0); if (hasServerMetrics) { rideLifecycle.updateDriverRouteMetrics( etaSeconds: etaSeconds != null && etaSeconds > 0 ? etaSeconds : null, distanceMeters: distanceMeters, ); } rideLifecycle.updateDriverMarker(newPos, heading); rideLifecycle.updateRemainingRoute(newPos, updateEta: !hasServerMetrics); rideLifecycle.update(); } catch (e) { Log.print('Error in handleDriverLocationUpdate: $e'); } } void disposeRideSocket() { _heartbeatTimer?.cancel(); _watchdogTimer?.cancel(); if (_isSocketInitialized) { socket.disconnect(); socket.dispose(); isSocketConnected = false; _isSocketInitialized = false; Log.print("🔌 Socket Disposed"); } } @override void onClose() { disposeRideSocket(); super.onClose(); } }