feat: refactor financial wallet UI components and add offline map service support
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
||||
import 'package:socket_io_client/socket_io_client.dart' as IO;
|
||||
import 'package:flutter_overlay_window/flutter_overlay_window.dart' as Overlay;
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:geolocator/geolocator.dart' as geo;
|
||||
import '../../constant/box_name.dart';
|
||||
import '../firebase/local_notification.dart';
|
||||
|
||||
const String notificationChannelId = 'driver_service_channel';
|
||||
const int notificationId = 888;
|
||||
@@ -35,14 +37,18 @@ Future<bool> onStart(ServiceInstance service) async {
|
||||
IO.OptionBuilder()
|
||||
.setTransports(['websocket'])
|
||||
.disableAutoConnect()
|
||||
.setQuery({'driver_id': driverId, 'token': token})
|
||||
.setQuery({
|
||||
'driver_id': driverId,
|
||||
'token': token,
|
||||
'EIO': '3', // توافقية مع Workerman
|
||||
})
|
||||
.setReconnectionAttempts(double.infinity)
|
||||
.build());
|
||||
|
||||
socket.connect();
|
||||
|
||||
socket.onConnect((_) {
|
||||
print("✅ Background Service: Socket Connected!");
|
||||
print("✅ Background Service: Socket Connected! ID: ${socket?.id}");
|
||||
if (service is AndroidServiceInstance) {
|
||||
flutterLocalNotificationsPlugin.show(
|
||||
id: notificationId,
|
||||
@@ -70,39 +76,94 @@ Future<bool> onStart(ServiceInstance service) async {
|
||||
final box = GetStorage();
|
||||
bool isAppInForeground = box.read(BoxName.isAppInForeground) ?? false;
|
||||
|
||||
// 🔥 Check إضافي: هل الـ Overlay مفتوح بالفعل؟
|
||||
bool overlayActive = await Overlay.FlutterOverlayWindow.isActive();
|
||||
// 🔥 Check إضافي: هل الـ Overlay مفتوح بالفعل؟ (للأندرويد فقط)
|
||||
bool overlayActive = false;
|
||||
if (Platform.isAndroid) {
|
||||
overlayActive = await Overlay.FlutterOverlayWindow.isActive();
|
||||
}
|
||||
|
||||
if (isAppInForeground || overlayActive) {
|
||||
print("🛑 App is FOREGROUND or Overlay already shown. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
// عرض الـ Overlay
|
||||
print("🚀 App is BACKGROUND. Showing Overlay...");
|
||||
try {
|
||||
await Overlay.FlutterOverlayWindow.showOverlay(
|
||||
enableDrag: true,
|
||||
overlayTitle: "طلب جديد",
|
||||
overlayContent: "لديك طلب جديد وصل للتو!",
|
||||
flag: OverlayFlag.focusPointer,
|
||||
positionGravity: PositionGravity.auto,
|
||||
height: WindowSize.matchParent,
|
||||
width: WindowSize.matchParent,
|
||||
startPosition: const OverlayPosition(0, -30),
|
||||
);
|
||||
await Overlay.FlutterOverlayWindow.shareData(data);
|
||||
} catch (e) {
|
||||
print("Overlay Error: $e");
|
||||
// عرض الـ Overlay (للأندرويد فقط)
|
||||
if (Platform.isAndroid) {
|
||||
print("🚀 App is BACKGROUND. Showing Overlay...");
|
||||
try {
|
||||
await Overlay.FlutterOverlayWindow.showOverlay(
|
||||
enableDrag: true,
|
||||
overlayTitle: "طلب جديد",
|
||||
overlayContent: "لديك طلب جديد وصل للتو!",
|
||||
flag: OverlayFlag.focusPointer,
|
||||
positionGravity: PositionGravity.auto,
|
||||
height: WindowSize.matchParent,
|
||||
width: WindowSize.matchParent,
|
||||
startPosition: const OverlayPosition(0, -30),
|
||||
);
|
||||
await Overlay.FlutterOverlayWindow.shareData(data);
|
||||
} catch (e) {
|
||||
print("Overlay Error: $e");
|
||||
}
|
||||
} else if (Platform.isIOS) {
|
||||
// على iOS، نظهر إشعاراً عادياً لأن الـ Overlay غير موجود
|
||||
flutterLocalNotificationsPlugin.show(
|
||||
id: 1002,
|
||||
title: "طلب رحلة جديد 🚖",
|
||||
body: "لديك طلب رحلة جديد، افتح التطبيق للموافقة عليه",
|
||||
notificationDetails: const NotificationDetails(
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
),
|
||||
),
|
||||
payload: jsonEncode(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
service.on('stopService').listen((event) {
|
||||
socket?.disconnect();
|
||||
socket?.clearListeners();
|
||||
socket?.dispose();
|
||||
service.stopSelf();
|
||||
});
|
||||
|
||||
// 🔥 Location management in background isolate (Using Geolocator)
|
||||
geo.Position? latestPos;
|
||||
|
||||
// Listen to location changes continuously in the background
|
||||
geo.Geolocator.getPositionStream(
|
||||
locationSettings: geo.AndroidSettings(
|
||||
accuracy: geo.LocationAccuracy.high,
|
||||
distanceFilter: 10,
|
||||
intervalDuration: const Duration(seconds: 10),
|
||||
),
|
||||
).listen((pos) {
|
||||
latestPos = pos;
|
||||
});
|
||||
|
||||
// 🔥 MERCY HEARTBEAT: Send location every 2 minutes to keep driver active in 'raids'
|
||||
Timer.periodic(const Duration(minutes: 2), (timer) async {
|
||||
if (socket != null && socket.connected && latestPos != null) {
|
||||
try {
|
||||
socket.emit('update_location', {
|
||||
'driver_id': driverId,
|
||||
'lat': latestPos!.latitude,
|
||||
'lng': latestPos!.longitude,
|
||||
'heading': latestPos!.heading,
|
||||
'speed': latestPos!.speed * 3.6,
|
||||
'status': box.read(BoxName.statusDriverLocation) ?? 'on',
|
||||
'source': 'background_heartbeat'
|
||||
});
|
||||
print(
|
||||
"💓 Background Mercy Heartbeat Sent: ${latestPos!.latitude}, ${latestPos!.longitude}");
|
||||
} catch (e) {
|
||||
print("❌ Background Heartbeat Error: $e");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Timer.periodic(const Duration(seconds: 30), (timer) async {
|
||||
if (service is AndroidServiceInstance) {
|
||||
if (await service.isForegroundService()) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/*
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
@@ -234,3 +235,4 @@ class CameraClassController extends GetxController {
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -604,6 +604,59 @@ class CRUD {
|
||||
);
|
||||
return json.decode(response.body);
|
||||
}
|
||||
|
||||
Future<dynamic> getMapSaas({
|
||||
required String link,
|
||||
}) async {
|
||||
var url = Uri.parse(link);
|
||||
try {
|
||||
var response = await http.get(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': Env.mapSaasKey,
|
||||
},
|
||||
);
|
||||
Log.print('link -MapSaas: $link');
|
||||
Log.print('response -MapSaas: ${response.body}');
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
Log.print('MapSaas Error: ${response.statusCode} - ${response.body}');
|
||||
return null;
|
||||
} catch (e) {
|
||||
Log.print('MapSaas Exception: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> postMapSaas({
|
||||
required String link,
|
||||
required Map<String, dynamic> payload,
|
||||
}) async {
|
||||
var url = Uri.parse(link);
|
||||
try {
|
||||
var response = await http.post(
|
||||
url,
|
||||
body: jsonEncode(payload),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': Env.mapSaasKey,
|
||||
},
|
||||
);
|
||||
Log.print('post -MapSaas link: $link');
|
||||
Log.print('post -MapSaas payload: $payload');
|
||||
Log.print('post -MapSaas response: ${response.body}');
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
Log.print('MapSaas Post Error: ${response.statusCode} - ${response.body}');
|
||||
return null;
|
||||
} catch (e) {
|
||||
Log.print('MapSaas Post Exception: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NoInternetException implements Exception {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:get/get_core/src/get_main.dart';
|
||||
import 'package:get/get_navigation/src/extension_navigation.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||
|
||||
import 'background_service.dart';
|
||||
|
||||
@@ -28,18 +32,19 @@ class PermissionsHelper {
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
|
||||
// Android 13+ (API 33+) يحتاج إذن POST_NOTIFICATIONS
|
||||
if (androidInfo.version.sdkInt >= 33) {
|
||||
final status = await Permission.notification.request();
|
||||
|
||||
if (status.isDenied) {
|
||||
print('⚠️ إذن الإشعارات مرفوض');
|
||||
mySnackbarWarning(
|
||||
"يرجى منح صلاحية الإشعارات لضمان وصول الطلبات إليك");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (status.isPermanentlyDenied) {
|
||||
print('⚠️ إذن الإشعارات مرفوض بشكل دائم - افتح الإعدادات');
|
||||
await openAppSettings();
|
||||
mySnackbarWarning('يرجى فتح الإعدادات وتفعيل صلاحية الإشعارات');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -50,25 +55,31 @@ class PermissionsHelper {
|
||||
|
||||
/// طلب جميع الإذونات المطلوبة
|
||||
static Future<bool> requestAllPermissions() async {
|
||||
// إذن الإشعارات أولاً
|
||||
bool notificationGranted = await requestNotificationPermission();
|
||||
if (!notificationGranted) return false;
|
||||
// إذن الإشعارات (اختياري)
|
||||
await requestNotificationPermission();
|
||||
|
||||
// إذن الموقع
|
||||
final locationStatus = await Permission.location.request();
|
||||
if (!locationStatus.isGranted) {
|
||||
print('⚠️ إذن الموقع مرفوض');
|
||||
// 1. طلب إذن الموقع الأساسي فقط إذا كان مرفوضاً
|
||||
var status = await Permission.location.status;
|
||||
if (status.isDenied) {
|
||||
status = await Permission.location.request();
|
||||
}
|
||||
|
||||
if (status.isPermanentlyDenied) {
|
||||
_showSettingsDialog('الموقع');
|
||||
return false;
|
||||
}
|
||||
|
||||
// إذن الموقع في الخلفية
|
||||
if (Platform.isAndroid) {
|
||||
final bgLocationStatus = await Permission.locationAlways.request();
|
||||
if (!bgLocationStatus.isGranted) {
|
||||
print('⚠️ إذن الموقع في الخلفية مرفوض');
|
||||
}
|
||||
}
|
||||
return status.isGranted || status.isLimited;
|
||||
}
|
||||
|
||||
return true;
|
||||
static void _showSettingsDialog(String permissionName) {
|
||||
MyDialog().getDialog(
|
||||
'صلاحية $permissionName مطلوبة',
|
||||
'لقد قمت برفض صلاحية $permissionName سابقاً. يرجى تفعيلها من الإعدادات لتمكين التطبيق من العمل.',
|
||||
() async {
|
||||
await openAppSettings();
|
||||
Get.back();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:geolocator/geolocator.dart' as geo;
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.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 'package:trip_overlay_plugin/trip_overlay_plugin.dart';
|
||||
@@ -17,6 +15,7 @@ import '../../constant/box_name.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import '../firebase/local_notification.dart';
|
||||
import '../home/captin/home_captain_controller.dart';
|
||||
import '../home/captin/map_driver_controller.dart';
|
||||
import '../home/payment/captain_wallet_controller.dart';
|
||||
@@ -63,6 +62,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
bool _isPowerSavingMode = false;
|
||||
|
||||
final List<Map<String, dynamic>> _trackBuffer = [];
|
||||
final List<Map<String, dynamic>> _behaviorBuffer = [];
|
||||
|
||||
LatLng? _lastPosForDistance;
|
||||
LatLng? _lastRecordedRealLoc;
|
||||
@@ -137,26 +137,22 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
Log.print("📱 Lifecycle: App is in FOREGROUND");
|
||||
box.write(BoxName.isAppInForeground, true);
|
||||
|
||||
// إيقاف خدمة الخلفية لأننا في الواجهة الآن
|
||||
// إيقاف خدمة الخلفية
|
||||
BackgroundServiceHelper.stopService();
|
||||
|
||||
// التأكد من أن السوكيت متصل، وإذا لا، نعيد الاتصال فوراً
|
||||
if (socket == null || !socket!.connected) {
|
||||
Log.print("🔄 Socket disconnected in background. Reconnecting now...");
|
||||
|
||||
if (socket == null || (!socket!.connected && !_isInitializingSocket)) {
|
||||
Log.print("🔄 Initializing Socket on resume...");
|
||||
initSocket();
|
||||
} else {
|
||||
// إذا كان متصلاً، ننعش المستمعين فقط للتأكد
|
||||
_setupSocketListeners();
|
||||
}
|
||||
} else if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.detached) {
|
||||
Log.print("📱 Lifecycle: App is in BACKGROUND");
|
||||
box.write(BoxName.isAppInForeground, false);
|
||||
|
||||
// تشغيل خدمة الخلفية لضمان بقاء التطبيق حياً
|
||||
BackgroundServiceHelper.startService();
|
||||
|
||||
// ملاحظة: لا نقطع السوكيت هنا، نتركه يعمل قدر الإمكان
|
||||
// تشغيل خدمة الخلفية للأندرويد لضمان بقاء التطبيق حياً
|
||||
if (!Platform.isIOS) {
|
||||
BackgroundServiceHelper.startService();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,69 +175,107 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
// ====== Socket Logic (Improved) ======
|
||||
// ===================================================================
|
||||
|
||||
bool _isInitializingSocket = false;
|
||||
|
||||
void initSocket() {
|
||||
// منع الاستدعاءات المتداخلة التي تسبب قتل الاتصال قبل اكتماله
|
||||
if (_isInitializingSocket) {
|
||||
Log.print("⏳ Socket is already initializing. Skipping redundant call.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (socket != null && socket!.connected) {
|
||||
Log.print("✅ Socket is already connected. No need to re-init.");
|
||||
return;
|
||||
}
|
||||
|
||||
String driverId = box.read(BoxName.driverID).toString();
|
||||
String token = box.read(BoxName.tokenDriver).toString();
|
||||
String platform = Platform.isIOS ? 'ios' : 'android';
|
||||
|
||||
// 1. إذا كان السوكيت موجوداً، فقط تأكد من اتصاله
|
||||
_isInitializingSocket = true;
|
||||
|
||||
// تنظيف السوكيت القديم فقط إذا كان موجوداً وغير متصل
|
||||
if (socket != null) {
|
||||
if (!socket!.connected) {
|
||||
Log.print("🟡 Socket exists but disconnected. Reconnecting...");
|
||||
socket!.connect();
|
||||
}
|
||||
_setupSocketListeners(); // تحديث المستمعين
|
||||
return;
|
||||
Log.print("🧹 Cleaning up old socket instance...");
|
||||
socket!.clearListeners();
|
||||
socket!.dispose();
|
||||
socket = null;
|
||||
}
|
||||
|
||||
Log.print(
|
||||
"🟡 [LocationController] Creating NEW Socket for Driver: $driverId");
|
||||
"🟡 [LocationController] Initializing 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());
|
||||
try {
|
||||
// العودة للـ Websocket حصراً لأنه الوحيد الذي ينجح في فتح القناة
|
||||
socket = IO.io(
|
||||
'https://location.intaleq.xyz',
|
||||
IO.OptionBuilder()
|
||||
.setTransports(['websocket'])
|
||||
.setQuery({'driver_id': driverId, 'token': token, 'EIO': '3'})
|
||||
.enableForceNew()
|
||||
.build());
|
||||
|
||||
socket!.connect();
|
||||
_setupSocketListeners();
|
||||
_setupSocketListeners();
|
||||
socket!.connect();
|
||||
} catch (e) {
|
||||
_isInitializingSocket = false;
|
||||
Log.print("❌ Socket Initialization Exception: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// دالة منفصلة لضمان عدم تكرار المستمعين
|
||||
void _setupSocketListeners() {
|
||||
if (socket == null) return;
|
||||
|
||||
// تنظيف القديم أولاً
|
||||
socket!.off('connect');
|
||||
socket!.off('disconnect');
|
||||
socket!.off('new_ride_request');
|
||||
socket!.off('ride_cancelled');
|
||||
socket!.off('connect_error');
|
||||
socket!.off('error');
|
||||
|
||||
socket!.onConnect((_) {
|
||||
Log.print('✅ Socket Connected! ID: ${socket?.id}');
|
||||
isSocketConnected = true;
|
||||
_startHeartbeat();
|
||||
_isInitializingSocket = false;
|
||||
|
||||
// ننتظر قليلاً للتأكد من تعبئة الـ IDs
|
||||
Future.delayed(const Duration(milliseconds: 1000), () {
|
||||
String? sid = socket?.id;
|
||||
String? eid = socket?.io.engine?.id;
|
||||
|
||||
Log.print(
|
||||
'✅ Socket Connected! ID: ${sid ?? eid ?? 'N/A'} (SID: $sid, EID: $eid)');
|
||||
|
||||
if (sid != null || eid != null) {
|
||||
isSocketConnected = true;
|
||||
_startHeartbeat();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket!.onDisconnect((_) {
|
||||
Log.print('❌ Socket Disconnected');
|
||||
socket!.onDisconnect((data) {
|
||||
_isInitializingSocket = false;
|
||||
Log.print('❌ Socket Disconnected: $data');
|
||||
isSocketConnected = false;
|
||||
_stopHeartbeat();
|
||||
});
|
||||
|
||||
socket!.onConnectError((err) {
|
||||
_isInitializingSocket = false;
|
||||
Log.print('❌ Socket Connect Error: $err');
|
||||
});
|
||||
|
||||
socket!.onConnectTimeout((data) {
|
||||
_isInitializingSocket = false;
|
||||
Log.print('❌ Socket Connect Timeout: $data');
|
||||
});
|
||||
|
||||
socket!.onError((err) {
|
||||
_isInitializingSocket = false;
|
||||
Log.print('❌ Socket General Error: $err');
|
||||
});
|
||||
|
||||
socket!.on('reconnect_attempt', (attempt) {
|
||||
Log.print('🔄 Socket Reconnecting... Attempt: $attempt');
|
||||
});
|
||||
|
||||
// 🔥 الاستماع للطلبات الجديدة
|
||||
socket!.on('new_ride_request', (data) {
|
||||
Log.print("🔔 Socket: New Ride Request Arrived!");
|
||||
@@ -296,8 +330,17 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
|
||||
if (!isAppInForeground) {
|
||||
Log.print(
|
||||
"🛑 التطبيق في الخلفية. السوكيت سيتجاهل التوجيه ويترك المهمة للـ Overlay.");
|
||||
return; // 👈 هذا السطر يمنع السوكيت من إكمال العمل وفتح الصفحة
|
||||
"📱 [LocationController] Order received in background (iOS/Android). Source: $source");
|
||||
|
||||
if (Platform.isIOS) {
|
||||
// على iOS، نقوم بإظهار إشعار محلي لأن الـ Overlay غير مدعوم
|
||||
NotificationController().showNotification(
|
||||
"طلب رحلة جديد 🚖",
|
||||
"لديك طلب رحلة جديد، افتح التطبيق للموافقة عليه",
|
||||
jsonEncode(rideData),
|
||||
'ding.wav');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -309,14 +352,23 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
|
||||
// 3. تجهيز البيانات (DriverList)
|
||||
List<dynamic> driverList = [];
|
||||
if (rideData.length > 0) {
|
||||
var sortedKeys = rideData.keys.map((e) => int.tryParse(e) ?? 0).toList()
|
||||
..sort();
|
||||
if (rideData.isNotEmpty) {
|
||||
var sortedKeys = rideData.keys
|
||||
.where((e) => int.tryParse(e) != null)
|
||||
.map((e) => int.parse(e))
|
||||
.toList()..sort();
|
||||
|
||||
for (var key in sortedKeys) {
|
||||
driverList.add(rideData[key.toString()]);
|
||||
}
|
||||
}
|
||||
|
||||
// الحماية ضد البنية غير المكتملة
|
||||
if (driverList.length <= 16) {
|
||||
Log.print("❌ Socket Error: Parsed driver list is incomplete.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. إغلاق النافذة (إن وجدت بالخطأ) والتنقل
|
||||
try {
|
||||
if (await TripOverlayPlugin.isOverlayActive()) {
|
||||
@@ -459,21 +511,16 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
_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();
|
||||
if (socket != null) {
|
||||
socket!.clearListeners();
|
||||
socket!
|
||||
.dispose(); // استخدام dispose بدلاً من disconnect لضمان تحرير الموارد على iOS
|
||||
}
|
||||
|
||||
if (!Platform.isIOS) {
|
||||
await BackgroundServiceHelper.stopService();
|
||||
}
|
||||
|
||||
await BackgroundServiceHelper.stopService();
|
||||
socket = null;
|
||||
isSocketConnected = false;
|
||||
_isReady = false;
|
||||
@@ -526,16 +573,31 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
|
||||
Future<void> _flushBufferToServer() async {
|
||||
if (_trackBuffer.isEmpty) return;
|
||||
List<Map<String, dynamic>> batch = List.from(_trackBuffer);
|
||||
_trackBuffer.clear();
|
||||
|
||||
int itemsToTake = _trackBuffer.length > 100 ? 100 : _trackBuffer.length;
|
||||
List<Map<String, dynamic>> batch = _trackBuffer.sublist(0, itemsToTake);
|
||||
|
||||
final String driverId = (box.read(BoxName.driverID) ?? '').toString();
|
||||
try {
|
||||
await CRUD().post(
|
||||
var res = await CRUD().post(
|
||||
link: '${AppLink.locationServer}/add_batch.php',
|
||||
payload: {'driver_id': driverId, 'batch_data': jsonEncode(batch)},
|
||||
);
|
||||
if (res != 'failure') {
|
||||
_trackBuffer.removeRange(0, itemsToTake);
|
||||
} else {
|
||||
_enforceBufferLimit();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('❌ Failed to upload batch: $e');
|
||||
_enforceBufferLimit();
|
||||
}
|
||||
}
|
||||
|
||||
void _enforceBufferLimit() {
|
||||
if (_trackBuffer.length > 500) {
|
||||
_trackBuffer.removeRange(0, _trackBuffer.length - 500);
|
||||
Log.print("⚠️ Buffer limit enforced. Removed oldest entries.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,32 +609,65 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
if (level >= powerSaveExitLevel) _isPowerSavingMode = false;
|
||||
if (previousMode != _isPowerSavingMode) {
|
||||
_startBatchTimers();
|
||||
startLocationUpdates();
|
||||
_updateLocationSettings();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _updateLocationSettings() async {
|
||||
if (_locSub == null) return;
|
||||
int interval = _isPowerSavingMode ? 10000 : 5000;
|
||||
try {
|
||||
await location.changeSettings(
|
||||
accuracy: LocationAccuracy.navigation,
|
||||
interval: interval,
|
||||
distanceFilter: _isPowerSavingMode ? 20 : 10,
|
||||
);
|
||||
Log.print("🔋 Location settings updated. Power Save: $_isPowerSavingMode");
|
||||
} catch (e) {
|
||||
Log.print("❌ Failed to update location settings: $e");
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
Log.print('SQLite Error: $e');
|
||||
_lastSqlLoc = pos;
|
||||
|
||||
_behaviorBuffer.add({
|
||||
'driver_id': (box.read(BoxName.driverID) ?? '').toString(),
|
||||
'latitude': pos.latitude,
|
||||
'longitude': pos.longitude,
|
||||
'acceleration': accel,
|
||||
'created_at': now.toIso8601String(),
|
||||
'updated_at': now.toIso8601String(),
|
||||
});
|
||||
|
||||
if (_behaviorBuffer.length >= 10) {
|
||||
_flushBehaviorBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
void _flushBehaviorBuffer() {
|
||||
if (_behaviorBuffer.isEmpty) return;
|
||||
List<Map<String, dynamic>> batch = List.from(_behaviorBuffer);
|
||||
_behaviorBuffer.clear();
|
||||
|
||||
Future.microtask(() async {
|
||||
try {
|
||||
for (var data in batch) {
|
||||
await sql.insertData(data, TableName.behavior);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('SQLite Batch Insert Error: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// استبدال دالة Haversine اليدوية بـ Geolocator في باقي الكود أيضاً
|
||||
// لأنها تعتمد على C++ في الأندرويد و Obj-C في الآيفون (Native Speed)
|
||||
double _calculateDistance(LatLng a, LatLng b) {
|
||||
|
||||
@@ -217,8 +217,12 @@ class SecurityHelper {
|
||||
isNotTrust = await JailbreakRootDetection.instance.isNotTrust;
|
||||
isJailBroken = await JailbreakRootDetection.instance.isJailBroken;
|
||||
isRealDevice = await JailbreakRootDetection.instance.isRealDevice;
|
||||
isOnExternalStorage =
|
||||
await JailbreakRootDetection.instance.isOnExternalStorage;
|
||||
|
||||
// This method is only relevant/implemented for Android
|
||||
if (Platform.isAndroid) {
|
||||
isOnExternalStorage =
|
||||
await JailbreakRootDetection.instance.isOnExternalStorage;
|
||||
}
|
||||
|
||||
List<JailbreakIssue> issues =
|
||||
await JailbreakRootDetection.instance.checkForIssues;
|
||||
@@ -230,7 +234,6 @@ class SecurityHelper {
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
bundleId = packageInfo.packageName;
|
||||
if (bundleId.isNotEmpty) {
|
||||
// Pass the CORRECT bundle ID to isTampered
|
||||
isTampered = await JailbreakRootDetection.instance.isTampered(bundleId);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user