|
|
|
@@ -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';
|
|
|
|
@@ -25,6 +26,37 @@ import 'background_service.dart';
|
|
|
|
|
import 'crud.dart';
|
|
|
|
|
import '../transit/transit_driver_controller.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 {
|
|
|
|
|
// ===================================================================
|
|
|
|
|
// ====== Tunables ======
|
|
|
|
@@ -38,6 +70,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 ======
|
|
|
|
|
// ===================================================================
|
|
|
|
@@ -84,6 +139,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 = [];
|
|
|
|
|
|
|
|
|
@@ -130,6 +196,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
|
|
|
|
_isReady = true;
|
|
|
|
|
|
|
|
|
|
initSocket();
|
|
|
|
|
await _detectGmsAvailability();
|
|
|
|
|
await _initLocationSettings();
|
|
|
|
|
_listenToBatteryChanges();
|
|
|
|
|
|
|
|
|
@@ -163,10 +230,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");
|
|
|
|
@@ -555,13 +627,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;
|
|
|
|
@@ -577,6 +645,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);
|
|
|
|
@@ -635,6 +708,8 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
|
|
|
|
|
|
|
|
|
_locSub?.cancel();
|
|
|
|
|
_locSub = null;
|
|
|
|
|
_appliedProfileSignature = null;
|
|
|
|
|
_stationarySince = null;
|
|
|
|
|
_recordTimer?.cancel();
|
|
|
|
|
_uploadBatchTimer?.cancel();
|
|
|
|
|
_socketHeartbeat?.cancel();
|
|
|
|
@@ -746,24 +821,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");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -832,10 +1006,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");
|
|
|
|
|
}
|
|
|
|
|