Files
intaleq_driver/lib/controller/functions/location_controller.dart
Hamza-Ayed fbfde115a8 26-1-21/2
2026-01-21 17:08:25 +03:00

635 lines
21 KiB
Dart
Executable File

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<LocationData>? _locSub;
StreamSubscription<BatteryState>? _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<Map<String, dynamic>> _trackBuffer = [];
LatLng? _lastPosForDistance;
LatLng? _lastRecordedRealLoc;
DateTime? _lastRecordedTime;
LatLng? _lastSqlLoc;
double? _lastSpeed;
DateTime? _lastSpeedAt;
@override
Future<void> 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<bool> _awaitDependencies() async {
int attempts = 0;
while (attempts < 10) {
if (Get.isRegistered<HomeCaptainController>() &&
Get.isRegistered<CaptainWalletController>()) {
_homeCtrl = Get.find<HomeCaptainController>();
_walletCtrl = Get.find<CaptainWalletController>();
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<dynamic> 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<String, dynamic> 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<MapDriverController>()) {
Get.find<MapDriverController>()
.processRideCancelledByPassenger(reason, source: "Socket");
}
});
}
// داخل LocationController
Future<void> handleIncomingOrder(
Map<String, dynamic> 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<dynamic> 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<MapDriverController>()) {
final mapCtrl = Get.find<MapDriverController>();
// 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<void> 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<void> _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<void> 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<void> _flushBufferToServer() async {
if (_trackBuffer.isEmpty) return;
List<Map<String, dynamic>> 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<void> _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<void> _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<bool> _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<LocationData?> getLocation() async {
try {
if (await _ensureServiceAndPermission()) {
return await location.getLocation();
}
} catch (e) {
print('❌ FAILED to get single location: $e');
}
return null;
}
}