Update codebase
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user