Update codebase

This commit is contained in:
Hamza-Ayed
2026-08-09 16:56:13 +03:00
parent 95e2e4f35d
commit b64debaa88
1058 changed files with 164327 additions and 113928 deletions
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:geolocator/geolocator.dart' as geo;
import 'package:intaleq_maps/intaleq_maps.dart';
@@ -9,7 +10,7 @@ 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:tripz_driver/constant/table_names.dart';
import 'package:siro_driver/constant/table_names.dart';
import 'package:trip_overlay_plugin/trip_overlay_plugin.dart';
import '../../constant/country_polygons.dart';
import '../../constant/box_name.dart';
@@ -24,6 +25,38 @@ import '../home/navigation/navigation_controller.dart';
import 'background_service.dart';
import 'crud.dart';
import '../transit/transit_driver_controller.dart';
import '../food_delivery/food_location_broadcaster.dart';
/// إعدادات تتبّع الموقع لحالة معيّنة من حالات السائق.
/// الهدف: عدم تشغيل الـ GPS بأقصى طاقته إلا أثناء الرحلة الفعلية.
class _LocationProfile {
final String name;
final LocationAccuracy accuracy;
final int interval; // ميلي ثانية
final double distanceFilter; // متر
const _LocationProfile(
this.name, this.accuracy, this.interval, this.distanceFilter);
_LocationProfile copyWith(
{String? name,
LocationAccuracy? accuracy,
int? interval,
double? distanceFilter}) =>
_LocationProfile(
name ?? this.name,
accuracy ?? this.accuracy,
interval ?? this.interval,
distanceFilter ?? this.distanceFilter,
);
/// بصمة تُستخدم لتفادي استدعاء changeSettings بلا داعٍ.
String get signature => '$accuracy|$interval|$distanceFilter';
@override
String toString() =>
'$name(acc: ${accuracy.name}, ${interval}ms, ${distanceFilter}m)';
}
class LocationController extends GetxController with WidgetsBindingObserver {
// ===================================================================
@@ -38,6 +71,29 @@ class LocationController extends GetxController with WidgetsBindingObserver {
static const int powerSaveTriggerLevel = 20;
static const int powerSaveExitLevel = 25;
// ===================================================================
// ====== ملفات التتبّع التكيّفية (أ) ======
// ===================================================================
/// السائق غير متاح (off/blocked): أرخص إعداد ممكن مع إبقاء آخر موقع معروف.
static const _profileIdle =
_LocationProfile('idle', LocationAccuracy.balanced, 30000, 100);
/// متصل وينتظر طلباً: لا حاجة لدقة الملاحة — يكفي أن يعرفه السيرفر بالحي.
static const _profileWaiting =
_LocationProfile('waiting', LocationAccuracy.high, 15000, 50);
/// قَبِل الطلب وفي طريقه للراكب: الراكب يراقب السهم، نحتاج تحديثاً معقولاً.
static const _profileEnRoute =
_LocationProfile('enRoute', LocationAccuracy.high, 5000, 15);
/// رحلة جارية (أو وضع الباص): أعلى دقة — هنا تُحتسب المسافة والأجرة.
static const _profileOnTrip =
_LocationProfile('onTrip', LocationAccuracy.navigation, 4000, 10);
/// بعد هذه المدة من السكون نضاعف الفترة (سائق واقف في مرآب/إشارة طويلة).
static const Duration stationaryGrace = Duration(seconds: 90);
static const double stationarySpeedThreshold = 1.0; // م/ث ≈ 3.6 كم/س
// ===================================================================
// ====== Services & Variables ======
// ===================================================================
@@ -62,8 +118,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
box.write(BoxName.isBusMode, enabled);
box.write(BoxName.busModeTripId, enabled ? tripId : null);
box.write(BoxName.busModeRouteId, enabled ? routeId : null);
Log.print(
'🚌 Bus mode ${enabled ? "enabled" : "disabled"} (trip: $tripId, route: $routeId)');
Log.print('🚌 Bus mode ${enabled ? "enabled" : "disabled"} (trip: $tripId, route: $routeId)');
}
StreamSubscription<LocationData>? _locSub;
@@ -85,6 +140,17 @@ class LocationController extends GetxController with WidgetsBindingObserver {
bool _isReady = false;
bool _isPowerSavingMode = false;
/// هل خدمات جوجل متوفرة على الجهاز؟ (ج)
/// null = لم يُفحص بعد. على أجهزة هواوي بلا GMS يسقط الباكيج إلى
/// LocationManager الخام: دقة أقل واستهلاك بطارية أعلى ⇒ نخفّف الإعدادات.
bool? _gmsAvailable;
static const MethodChannel _deviceServicesChannel =
MethodChannel('com.siro.siro_driver/device_services');
/// آخر إعداد طُبّق فعلياً — لتفادي استدعاء changeSettings بلا تغيير.
String? _appliedProfileSignature;
DateTime? _stationarySince;
final List<Map<String, dynamic>> _trackBuffer = [];
final List<Map<String, dynamic>> _behaviorBuffer = [];
@@ -131,6 +197,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
_isReady = true;
initSocket();
await _detectGmsAvailability();
await _initLocationSettings();
_listenToBatteryChanges();
@@ -164,10 +231,15 @@ class LocationController extends GetxController with WidgetsBindingObserver {
// إيقاف خدمة الخلفية
BackgroundServiceHelper.stopService();
if (socket == null || (!socket!.connected && !_isInitializingSocket)) {
Log.print("🔄 Initializing Socket on resume...");
initSocket();
// 🔥 Fix: iOS silently kills WebSockets in background.
// The socket_io_client might still report 'connected' as true for ~20s.
// We MUST force a disconnect so initSocket() properly creates a fresh connection
// and immediately emits 'get_pending_orders' to catch missed FCM payloads.
if (socket != null) {
socket!.disconnect();
}
initSocket();
} else if (state == AppLifecycleState.paused ||
state == AppLifecycleState.detached) {
Log.print("📱 Lifecycle: App is in BACKGROUND");
@@ -227,8 +299,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
socket = null;
}
print(
"🟡 [LocationController] Initializing NEW Socket for Driver: $driverId");
print("🟡 [LocationController] Initializing NEW Socket for Driver: $driverId");
try {
// العودة للـ Websocket حصراً لأنه الوحيد الذي ينجح في فتح القناة
@@ -278,12 +349,12 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (sid != null || eid != null) {
isSocketConnected = true;
if (myLocation.latitude != 0) {
print("🚀 Sending initial location to socket after connect...");
emitLocationToSocket(myLocation, heading, speed);
}
_startHeartbeat();
// 🆕 طلب أي رحلات انتظار فاتتنا أثناء انقطاع الاتصال
@@ -462,8 +533,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
void _startHeartbeat() {
_socketHeartbeat?.cancel();
_socketHeartbeat = Timer.periodic(const Duration(seconds: 25), (timer) {
if (socket == null || !isSocketConnected || myLocation.latitude == 0)
return;
if (socket == null || !isSocketConnected || myLocation.latitude == 0) return;
if (isBusMode) {
emitBusLocationToSocket(myLocation, heading, speed);
@@ -515,8 +585,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
}
// 🚌 مواصلاتي — بثّ موقع الباص (بديل عن update_location أثناء وضع الباص)
void emitBusLocationToSocket(LatLng pos, double head, double spd,
{int? currentStopSeq}) {
void emitBusLocationToSocket(LatLng pos, double head, double spd, {int? currentStopSeq}) {
if (activeBusTripId == null || activeBusRouteId == null) return;
if (socket == null || !socket!.connected) return;
@@ -559,13 +628,9 @@ class LocationController extends GetxController with WidgetsBindingObserver {
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,
);
// مصدر الحقيقة الوحيد لإعدادات الموقع: _applyProfile.
await _applyProfile(force: true);
_locSub = location.onLocationChanged.listen((LocationData loc) async {
if (loc.latitude == null || loc.longitude == null) return;
@@ -581,6 +646,11 @@ class LocationController extends GetxController with WidgetsBindingObserver {
speed = loc.speed ?? 0.0;
heading = loc.heading ?? 0.0;
// إعادة تقييم ملف التتبّع مع كل قراءة — لا يستدعي changeSettings
// إلا إذا تغيّرت الحالة فعلاً (حالة السائق/الرحلة/السكون/البطارية).
_trackStationary();
await _applyProfile();
box.write('last_lat', pos.latitude);
box.write('last_lng', pos.longitude);
box.write('last_heading', heading);
@@ -628,6 +698,15 @@ class LocationController extends GetxController with WidgetsBindingObserver {
navCtrl.handleLocationUpdateFromCentral(pos, speed, heading);
}
// بثّ الموقع للزبون أثناء مهمة توصيل — يركب على هذه النبضة بدل تدفّق
// GPS ثانٍ، فيستمر بعد إغلاق شاشة التوصيل ما دامت هذه الخدمة حية.
// يخرج فوراً إن لم تكن هناك مهمة نشطة.
FoodLocationBroadcaster.maybeSend(
lat: pos.latitude,
lng: pos.longitude,
heading: heading,
);
await _saveBehaviorIfMoved(pos, now, currentSpeed: speed);
}, onError: (e) => Log.print('❌ Location Stream Error: $e'));
}
@@ -639,6 +718,8 @@ class LocationController extends GetxController with WidgetsBindingObserver {
_locSub?.cancel();
_locSub = null;
_appliedProfileSignature = null;
_stationarySince = null;
_recordTimer?.cancel();
_uploadBatchTimer?.cancel();
_socketHeartbeat?.cancel();
@@ -712,6 +793,11 @@ class LocationController extends GetxController with WidgetsBindingObserver {
}
}
/// دفع ما تراكم من نقاط المسار فوراً بدل انتظار المؤقّت.
/// تستدعيها شاشة الملاحة عند انتهاء الملاحة، بعد أن صار الرفع مركزيّاً هنا
/// وحده (كان الملّاح يرفع دفعة موازية إلى نفس الواجهة فيتضاعف الحساب).
Future<void> flushTrackBufferNow() => _flushBufferToServer();
Future<void> _flushBufferToServer() async {
if (_trackBuffer.isEmpty) return;
@@ -750,24 +836,123 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (level >= powerSaveExitLevel) _isPowerSavingMode = false;
if (previousMode != _isPowerSavingMode) {
_startBatchTimers();
_updateLocationSettings();
_applyProfile();
}
});
}
Future<void> _updateLocationSettings() async {
if (_locSub == null) return;
int interval = _isPowerSavingMode ? 10000 : 5000;
// ===================================================================
// ====== محرّك الملفات التكيّفية (أ + ج) ======
// ===================================================================
/// (ج) فحص توفّر Google Play Services مرة واحدة عند الإقلاع.
/// النتيجة تُحفظ في الصندوق ليقرأها الـ background isolate أيضاً،
/// وتُبَث مع بيانات السائق لمعرفة حجم شريحة الأجهزة بلا GMS.
Future<void> _detectGmsAvailability() async {
if (!Platform.isAndroid) {
_gmsAvailable = true; // iOS: CoreLocation دائماً متاح
return;
}
try {
final bool available =
await _deviceServicesChannel.invokeMethod<bool>('isGmsAvailable') ??
true;
_gmsAvailable = available;
box.write(BoxName.isGmsAvailable, available);
Log.print(available
? "✅ GMS available — FusedLocationProvider in use."
: "⚠️ No GMS (Huawei/AOSP) — falling back to raw LocationManager, "
"relaxing location profile to protect battery.");
} catch (e) {
// القناة غير مسجّلة (نسخة قديمة) — نفترض التوفّر ولا نغيّر السلوك.
_gmsAvailable = true;
Log.print("⚠️ GMS check failed, assuming available: $e");
}
}
/// تتبّع السكون: سائق واقف لا يحتاج قراءات متلاحقة.
void _trackStationary() {
if (speed >= stationarySpeedThreshold) {
_stationarySince = null;
} else {
_stationarySince ??= DateTime.now();
}
}
bool get _isStationary =>
_stationarySince != null &&
DateTime.now().difference(_stationarySince!) >= stationaryGrace;
/// يختار الملف المناسب من حالة السائق والرحلة، ثم يطبّق عليه
/// مُعدِّلات البطارية والسكون وغياب GMS.
_LocationProfile _resolveProfile() {
final String driverStatus =
box.read(BoxName.statusDriverLocation) ?? 'off';
final String rideStatus = (box.read(BoxName.rideStatus) ?? '').toString();
_LocationProfile p;
if (isBusMode) {
p = _profileOnTrip;
} else if (driverStatus == 'off' || driverStatus == 'blocked') {
p = _profileIdle;
} else if (rideStatus == 'Begin') {
p = _profileOnTrip;
} else if (rideStatus == 'Apply' || rideStatus == 'Arrived') {
p = _profileEnRoute;
} else {
p = _profileWaiting;
}
// وضع توفير الطاقة: ضاعف الفترة والمسافة، وانزل عن دقة الملاحة.
if (_isPowerSavingMode) {
p = p.copyWith(
name: '${p.name}+save',
accuracy: p.accuracy == LocationAccuracy.navigation
? LocationAccuracy.high
: p.accuracy,
interval: p.interval * 2,
distanceFilter: p.distanceFilter * 2,
);
}
// (ج) بلا GMS: كل قراءة تُشعل شريحة الـ GPS منفردة ⇒ خفّف التردد.
if (_gmsAvailable == false) {
p = p.copyWith(
name: '${p.name}+nogms',
interval: (p.interval * 1.5).round(),
distanceFilter: p.distanceFilter * 2,
);
}
// السكون المطوّل: ضاعف الفترة ثلاثاً — إلا في رحلة جارية (عدّاد الانتظار).
if (_isStationary && p.accuracy != LocationAccuracy.navigation) {
p = p.copyWith(
name: '${p.name}+idle',
interval: p.interval * 3,
);
}
return p;
}
/// يطبّق الملف الحالي. لا يستدعي changeSettings إلا عند تغيّر فعلي،
/// لأن كل استدعاء يعيد تشغيل مزوّد الموقع في الطبقة الأصلية.
Future<void> _applyProfile({bool force = false}) async {
if (_locSub == null && !force) return;
final p = _resolveProfile();
if (!force && p.signature == _appliedProfileSignature) return;
try {
await location.changeSettings(
accuracy: LocationAccuracy.navigation,
interval: interval,
distanceFilter: _isPowerSavingMode ? 20 : 10,
accuracy: p.accuracy,
interval: p.interval,
distanceFilter: p.distanceFilter,
);
Log.print(
"🔋 Location settings updated. Power Save: $_isPowerSavingMode");
_appliedProfileSignature = p.signature;
Log.print("📍 Location profile → $p");
} catch (e) {
Log.print("❌ Failed to update location settings: $e");
Log.print("❌ Failed to apply location profile $p: $e");
}
}
@@ -836,10 +1021,9 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (await _ensureServiceAndPermission()) {
try {
await location.enableBackgroundMode(enable: true);
location.changeSettings(
accuracy: LocationAccuracy.navigation,
interval: 1000,
distanceFilter: 10);
// (ب) لا نضبط الإعدادات هنا: كانت interval: 1000 تتعارض مع إعدادات
// _subscribeLocationStream وتُبقي الـ GPS مشتعلاً كل ثانية.
// مصدر الحقيقة الوحيد الآن هو _applyProfile.
} catch (e) {
Log.print("Warning: $e");
}