327 lines
10 KiB
Dart
327 lines
10 KiB
Dart
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<RideLifecycleController>();
|
|
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<RideLifecycleController>();
|
|
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<RideLifecycleController>();
|
|
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<RideLifecycleController>();
|
|
|
|
Map<String, dynamic>? driverInfo;
|
|
if (data['driver_info'] != null && data['driver_info'] is Map) {
|
|
driverInfo = Map<String, dynamic>.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<RideLifecycleController>();
|
|
|
|
var rawList = data['DriverList'];
|
|
List<dynamic> 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<RideLifecycleController>();
|
|
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<NearbyDriversController>();
|
|
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<MapEngineController>();
|
|
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'];
|
|
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();
|
|
}
|
|
}
|