25-10-2/1
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:sefer_driver/controller/functions/encrypt_decrypt.dart';
|
||||
import 'package:sefer_driver/controller/functions/network/net_guard.dart';
|
||||
import 'package:secure_string_operations/secure_string_operations.dart';
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
@@ -10,18 +13,18 @@ import 'package:sefer_driver/main.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:sefer_driver/env/env.dart';
|
||||
import 'package:sefer_driver/print.dart';
|
||||
|
||||
import '../../constant/api_key.dart';
|
||||
import '../../constant/char_map.dart';
|
||||
import '../../constant/info.dart';
|
||||
import '../../views/widgets/error_snakbar.dart';
|
||||
import '../../print.dart';
|
||||
import 'gemeni.dart';
|
||||
import 'network/connection_check.dart';
|
||||
import 'upload_image.dart';
|
||||
|
||||
class CRUD {
|
||||
final NetGuard _netGuard = NetGuard();
|
||||
final _client = http.Client();
|
||||
|
||||
/// Stores the signature of the last logged error to prevent duplicates.
|
||||
static String _lastErrorSignature = '';
|
||||
@@ -40,32 +43,24 @@ class CRUD {
|
||||
static Future<void> addError(
|
||||
String error, String details, String where) async {
|
||||
try {
|
||||
// Create a unique signature for the current error
|
||||
final currentErrorSignature = '$where-$error';
|
||||
final now = DateTime.now();
|
||||
|
||||
// Check if the same error occurred recently
|
||||
if (currentErrorSignature == _lastErrorSignature &&
|
||||
now.difference(_lastErrorTimestamp) < _errorLogDebounceDuration) {
|
||||
// If it's the same error within the debounce duration, ignore it.
|
||||
print("Debounced a duplicate error: $error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the signature and timestamp for the new error
|
||||
_lastErrorSignature = currentErrorSignature;
|
||||
_lastErrorTimestamp = now;
|
||||
|
||||
// Get user information for the error log
|
||||
final userId =
|
||||
box.read(BoxName.driverID) ?? box.read(BoxName.passengerID);
|
||||
final userType =
|
||||
box.read(BoxName.driverID) != null ? 'Driver' : 'passenger';
|
||||
box.read(BoxName.driverID) != null ? 'Driver' : 'Passenger';
|
||||
final phone = box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver);
|
||||
|
||||
// Send the error data to the server
|
||||
// Note: This is a fire-and-forget call. We don't await it or handle its response
|
||||
// to prevent an infinite loop if the addError endpoint itself is failing.
|
||||
// Fire-and-forget call to prevent infinite loops if the logger itself fails.
|
||||
CRUD().post(
|
||||
link: AppLink.addError,
|
||||
payload: {
|
||||
@@ -73,134 +68,106 @@ class CRUD {
|
||||
'userId': userId.toString(),
|
||||
'userType': userType,
|
||||
'phone': phone.toString(),
|
||||
'device': where, // The location of the error
|
||||
'details': details, // The detailed stack trace or context
|
||||
'device': where,
|
||||
'details': details,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// If logging the error itself fails, print to the console to avoid infinite loops.
|
||||
print("Failed to log error to server: $e");
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/// Centralized private method to handle all API requests.
|
||||
/// Includes retry logic, network checking, and standardized error handling.
|
||||
Future<dynamic> _makeRequest({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
required Map<String, String> headers,
|
||||
}) async {
|
||||
try {
|
||||
// 1. Wrap the http.post call directly with HttpRetry.sendWithRetry.
|
||||
// It will attempt the request immediately and retry on transient errors.
|
||||
var response = await HttpRetry.sendWithRetry(
|
||||
() {
|
||||
var url = Uri.parse(link);
|
||||
return http.post(
|
||||
url,
|
||||
body: payload,
|
||||
headers: headers,
|
||||
);
|
||||
},
|
||||
// Optional: you can customize retry behavior for each call
|
||||
maxRetries: 3,
|
||||
timeout: const Duration(seconds: 15),
|
||||
);
|
||||
// Log.print('response: ${response.body}');
|
||||
// Log.print('request: ${response.request}');
|
||||
// Log.print('payload: ${payload}');
|
||||
// ✅ All your existing logic for handling server responses remains the same.
|
||||
// This part is only reached if the network request itself was successful.
|
||||
// timeouts أقصر
|
||||
const connectTimeout = Duration(seconds: 6);
|
||||
const receiveTimeout = Duration(seconds: 10);
|
||||
|
||||
// Handle successful response (200 OK)
|
||||
if (response.statusCode == 200) {
|
||||
Future<http.Response> doPost() {
|
||||
final url = Uri.parse(link);
|
||||
// استخدم _client بدل http.post
|
||||
return _client
|
||||
.post(url, body: payload, headers: headers)
|
||||
.timeout(connectTimeout + receiveTimeout);
|
||||
}
|
||||
|
||||
http.Response response;
|
||||
try {
|
||||
// retry ذكي: محاولة واحدة إضافية فقط لأخطاء شبكة/5xx
|
||||
try {
|
||||
response = await doPost();
|
||||
} on SocketException catch (_) {
|
||||
// محاولة ثانية واحدة فقط
|
||||
response = await doPost();
|
||||
} on TimeoutException catch (_) {
|
||||
response = await doPost();
|
||||
}
|
||||
|
||||
final sc = response.statusCode;
|
||||
final body = response.body;
|
||||
Log.print('body: ${body}');
|
||||
Log.print('body: ${body}');
|
||||
|
||||
// 2xx
|
||||
if (sc >= 200 && sc < 300) {
|
||||
try {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['status'] == 'success') {
|
||||
return jsonData; // Return the full JSON object on success
|
||||
} else {
|
||||
if (jsonData['status'] == 'failure') {
|
||||
// return 'failure';
|
||||
} else {
|
||||
addError(
|
||||
'API Logic Error: ${jsonData['status']}',
|
||||
'Response: ${response.body}',
|
||||
'CRUD._makeRequest - $link',
|
||||
);
|
||||
}
|
||||
return jsonData['status']; // Return the specific status string
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
addError(
|
||||
'JSON Decode Error: $e',
|
||||
'Response Body: ${response.body}\nStack Trace: $stackTrace',
|
||||
'CRUD._makeRequest - $link',
|
||||
);
|
||||
final jsonData = jsonDecode(body);
|
||||
return jsonData; // لا تعيد 'success' فقط؛ أعِد الجسم كله
|
||||
} catch (e, st) {
|
||||
// لا تسجّل كخطأ شبكي لكل حالة؛ فقط معلومات
|
||||
addError('JSON Decode Error', 'Body: $body\n$st',
|
||||
'CRUD._makeRequest $link');
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
// Handle Unauthorized (401)
|
||||
else if (response.statusCode == 401) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['error'] == 'Token expired') {
|
||||
await Get.put(LoginDriverController()).getJWT();
|
||||
return 'token_expired';
|
||||
} else {
|
||||
addError(
|
||||
'Unauthorized Error: ${jsonData['error']}',
|
||||
'Status Code: 401',
|
||||
'CRUD._makeRequest - $link',
|
||||
);
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 401 → دع الطبقة العليا تتعامل مع التجديد
|
||||
if (sc == 401) {
|
||||
// لا تستدع getJWT هنا كي لا نضاعف الرحلات
|
||||
return 'token_expired';
|
||||
}
|
||||
// Handle all other non-successful status codes
|
||||
else {
|
||||
|
||||
// 5xx: لا تعِد المحاولة هنا (حاولنا مرة ثانية فوق)
|
||||
if (sc >= 500) {
|
||||
addError(
|
||||
'HTTP Error',
|
||||
'Status Code: ${response.statusCode}\nResponse Body: ${response.body}',
|
||||
'CRUD._makeRequest - $link',
|
||||
);
|
||||
'Server 5xx', 'SC: $sc\nBody: $body', 'CRUD._makeRequest $link');
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 4xx أخرى: أعد الخطأ بدون تسجيل مكرر
|
||||
return 'failure';
|
||||
} on SocketException {
|
||||
// 2. This block now catches the "no internet" case after all retries have failed.
|
||||
_netGuard.notifyOnce((title, msg) {
|
||||
mySnackeBarError(msg);
|
||||
});
|
||||
return 'no_internet'; // Return the specific status you were using before.
|
||||
} catch (e, stackTrace) {
|
||||
// 3. This is a general catch-all for any other unexpected errors.
|
||||
addError(
|
||||
'HTTP Request Exception: $e',
|
||||
'Stack Trace: $stackTrace',
|
||||
'CRUD._makeRequest - $link',
|
||||
);
|
||||
_netGuard.notifyOnce((title, msg) => mySnackeBarError(msg));
|
||||
return 'no_internet';
|
||||
} on TimeoutException {
|
||||
return 'failure';
|
||||
} catch (e, st) {
|
||||
addError('HTTP Request Exception: $e', 'Stack: $st',
|
||||
'CRUD._makeRequest $link');
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs a standard authenticated POST request.
|
||||
/// Automatically handles token renewal.
|
||||
Future<dynamic> post({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
// 1. Check if the token is expired
|
||||
// bool isTokenExpired = JwtDecoder.isExpired(X
|
||||
// .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
|
||||
// .toString()
|
||||
// .split(AppInformation.addd)[0]);
|
||||
|
||||
// // 2. If expired, get a new one
|
||||
// if (isTokenExpired) {
|
||||
// await LoginDriverController().getJWT();
|
||||
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
// if (JwtDecoder.isExpired(token)) {
|
||||
// await Get.put(LoginController()).getJWT();
|
||||
// token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
// }
|
||||
|
||||
// 3. Prepare the headers with the valid token
|
||||
final headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
'Authorization':
|
||||
'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}'
|
||||
'Authorization': 'Bearer $token'
|
||||
};
|
||||
|
||||
// 4. Make the request using the centralized helper
|
||||
return await _makeRequest(
|
||||
link: link,
|
||||
payload: payload,
|
||||
@@ -208,24 +175,68 @@ class CRUD {
|
||||
);
|
||||
}
|
||||
|
||||
/// Performs an authenticated POST request to the wallet endpoints.
|
||||
/// Uses a separate JWT and HMAC for authentication.
|
||||
/// Performs a standard authenticated GET request (using POST method as per original code).
|
||||
/// Automatically handles token renewal.
|
||||
Future<dynamic> get({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
var url = Uri.parse(
|
||||
link,
|
||||
);
|
||||
var response = await http.post(
|
||||
url,
|
||||
body: payload,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
'Authorization':
|
||||
'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}'
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['status'] == 'success') {
|
||||
return response.body;
|
||||
}
|
||||
|
||||
return jsonData['status'];
|
||||
} else if (response.statusCode == 401) {
|
||||
// Specifically handle 401 Unauthorized
|
||||
var jsonData = jsonDecode(response.body);
|
||||
|
||||
if (jsonData['error'] == 'Token expired') {
|
||||
// Show snackbar prompting to re-login
|
||||
await Get.put(LoginDriverController()).getJWT();
|
||||
// mySnackbarSuccess('please order now'.tr);
|
||||
return 'token_expired'; // Return a specific value for token expiration
|
||||
} else {
|
||||
// Other 401 errors
|
||||
addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401',
|
||||
url.toString());
|
||||
return 'failure';
|
||||
}
|
||||
} else {
|
||||
addError('Non-200 response code: ${response.statusCode}',
|
||||
'crud().post - Other', url.toString());
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs an authenticated POST request to wallet endpoints.
|
||||
Future<dynamic> postWallet({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
// 1. Get the specific JWT and HMAC for the wallet
|
||||
var jwt = await LoginDriverController().getJwtWallet();
|
||||
final hmac = box.read(BoxName.hmac);
|
||||
|
||||
// 2. Prepare the headers
|
||||
final headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
'Authorization': 'Bearer $jwt',
|
||||
'X-HMAC-Auth': hmac.toString(),
|
||||
};
|
||||
|
||||
// 3. Make the request using the centralized helper
|
||||
return await _makeRequest(
|
||||
link: link,
|
||||
payload: payload,
|
||||
@@ -233,6 +244,58 @@ class CRUD {
|
||||
);
|
||||
}
|
||||
|
||||
/// Performs an authenticated GET request to wallet endpoints (using POST).
|
||||
Future<dynamic> getWallet({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
var s = await LoginDriverController().getJwtWallet();
|
||||
final hmac = box.read(BoxName.hmac);
|
||||
var url = Uri.parse(
|
||||
link,
|
||||
);
|
||||
var response = await http.post(
|
||||
url,
|
||||
body: payload,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
'Authorization': 'Bearer $s',
|
||||
'X-HMAC-Auth': hmac.toString(),
|
||||
},
|
||||
);
|
||||
// Log.print('response.request: ${response.request}');
|
||||
// Log.print('response.body: ${response.body}');
|
||||
// Log.print('response.payload: ${payload}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['status'] == 'success') {
|
||||
return response.body;
|
||||
}
|
||||
|
||||
return jsonData['status'];
|
||||
} else if (response.statusCode == 401) {
|
||||
// Specifically handle 401 Unauthorized
|
||||
var jsonData = jsonDecode(response.body);
|
||||
|
||||
if (jsonData['error'] == 'Token expired') {
|
||||
// Show snackbar prompting to re-login
|
||||
await Get.put(LoginDriverController()).getJwtWallet();
|
||||
|
||||
return 'token_expired'; // Return a specific value for token expiration
|
||||
} else {
|
||||
// Other 401 errors
|
||||
addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401',
|
||||
url.toString());
|
||||
return 'failure';
|
||||
}
|
||||
} else {
|
||||
addError('Non-200 response code: ${response.statusCode}',
|
||||
'crud().post - Other', url.toString());
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> postWalletMtn(
|
||||
{required String link, Map<String, dynamic>? payload}) async {
|
||||
final s = await LoginDriverController().getJwtWallet();
|
||||
@@ -250,11 +313,6 @@ class CRUD {
|
||||
},
|
||||
);
|
||||
|
||||
print('req: ${response.request}');
|
||||
print('status: ${response.statusCode}');
|
||||
print('body: ${response.body}');
|
||||
print('payload: $payload');
|
||||
|
||||
Map<String, dynamic> wrap(String status, {Object? message, int? code}) {
|
||||
return {
|
||||
'status': status,
|
||||
@@ -305,122 +363,11 @@ class CRUD {
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> get({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
// bool isTokenExpired = JwtDecoder.isExpired(X
|
||||
// .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
|
||||
// .toString()
|
||||
// .split(AppInformation.addd)[0]);
|
||||
// // Log.print('isTokenExpired: ${isTokenExpired}');
|
||||
|
||||
// if (isTokenExpired) {
|
||||
// await LoginDriverController().getJWT();
|
||||
// }
|
||||
// await Get.put(LoginDriverController()).getJWT();
|
||||
var url = Uri.parse(
|
||||
link,
|
||||
);
|
||||
var response = await http.post(
|
||||
url,
|
||||
body: payload,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
'Authorization':
|
||||
'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}'
|
||||
},
|
||||
);
|
||||
// print(response.request);
|
||||
// Log.print('response.body: ${response.body}');
|
||||
// print(payload);
|
||||
if (response.statusCode == 200) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['status'] == 'success') {
|
||||
return response.body;
|
||||
}
|
||||
|
||||
return jsonData['status'];
|
||||
} else if (response.statusCode == 401) {
|
||||
// Specifically handle 401 Unauthorized
|
||||
var jsonData = jsonDecode(response.body);
|
||||
|
||||
if (jsonData['error'] == 'Token expired') {
|
||||
// Show snackbar prompting to re-login
|
||||
await Get.put(LoginDriverController()).getJWT();
|
||||
// mySnackbarSuccess('please order now'.tr);
|
||||
|
||||
return 'token_expired'; // Return a specific value for token expiration
|
||||
} else {
|
||||
// Other 401 errors
|
||||
// addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401');
|
||||
return 'failure';
|
||||
}
|
||||
} else {
|
||||
// addError('Non-200 response code: ${response.statusCode}',
|
||||
// 'crud().post - Other');
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> getWallet({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
var s = await LoginDriverController().getJwtWallet();
|
||||
final hmac = box.read(BoxName.hmac);
|
||||
// Log.print('hmac: ${hmac}');
|
||||
var url = Uri.parse(
|
||||
link,
|
||||
);
|
||||
var response = await http.post(
|
||||
url,
|
||||
body: payload,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
'Authorization': 'Bearer $s',
|
||||
'X-HMAC-Auth': hmac.toString(),
|
||||
},
|
||||
);
|
||||
// Log.print('response.request: ${response.request}');
|
||||
// Log.print('response.body: ${response.body}');
|
||||
// print(payload);
|
||||
if (response.statusCode == 200) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['status'] == 'success') {
|
||||
return response.body;
|
||||
}
|
||||
|
||||
return jsonData['status'];
|
||||
} else if (response.statusCode == 401) {
|
||||
// Specifically handle 401 Unauthorized
|
||||
var jsonData = jsonDecode(response.body);
|
||||
|
||||
if (jsonData['error'] == 'Token expired') {
|
||||
// Show snackbar prompting to re-login
|
||||
// await Get.put(LoginDriverController()).getJwtWallet();
|
||||
|
||||
return 'token_expired'; // Return a specific value for token expiration
|
||||
} else {
|
||||
// Other 401 errors
|
||||
// addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401');
|
||||
return 'failure';
|
||||
}
|
||||
} else {
|
||||
// addError('Non-200 response code: ${response.statusCode}',
|
||||
// 'crud().post - Other');
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
|
||||
// Future<dynamic> postWallet(
|
||||
// {required String link, Map<String, dynamic>? payload}) async {
|
||||
// var s = await LoginDriverController().getJwtWallet();
|
||||
// // Log.print('jwt: ${s}');
|
||||
// final hmac = box.read(BoxName.hmac);
|
||||
// // Log.print('hmac: ${hmac}');
|
||||
// var url = Uri.parse(link);
|
||||
// // Log.print('url: ${url}');
|
||||
// try {
|
||||
// // await LoginDriverController().getJWT();
|
||||
|
||||
@@ -433,9 +380,6 @@ class CRUD {
|
||||
// 'X-HMAC-Auth': hmac.toString(),
|
||||
// },
|
||||
// );
|
||||
// // Log.print('response.request:${response.request}');
|
||||
// // Log.print('response.body: ${response.body}');
|
||||
// // Log.print('payload:$payload');
|
||||
// if (response.statusCode == 200) {
|
||||
// try {
|
||||
// var jsonData = jsonDecode(response.body);
|
||||
@@ -491,9 +435,6 @@ class CRUD {
|
||||
// // 'Authorization': 'Bearer ${box.read(BoxName.jwt)}'
|
||||
// },
|
||||
// );
|
||||
// print(response.request);
|
||||
// Log.print('response.body: ${response.body}');
|
||||
// print(payload);
|
||||
// if (response.statusCode == 200) {
|
||||
// try {
|
||||
// var jsonData = jsonDecode(response.body);
|
||||
@@ -763,11 +704,8 @@ class CRUD {
|
||||
|
||||
// التحقق من النتيجة
|
||||
if (response.statusCode == 200) {
|
||||
print("✅ Email sent successfully.");
|
||||
} else {
|
||||
print("❌ Failed to send email. Status: ${response.statusCode}");
|
||||
final responseBody = await response.stream.bytesToString();
|
||||
print("Response body: $responseBody");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -867,8 +805,6 @@ class CRUD {
|
||||
url,
|
||||
body: payload,
|
||||
);
|
||||
Log.print('esponse.body: ${response.body}');
|
||||
Log.print('esponse.body: ${response.request}');
|
||||
var jsonData = jsonDecode(response.body);
|
||||
|
||||
if (jsonData['status'] == 'OK') {
|
||||
|
||||
@@ -4,241 +4,523 @@ import 'dart:math';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:location/location.dart';
|
||||
import 'package:sefer_driver/constant/table_names.dart';
|
||||
import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart';
|
||||
import 'package:battery_plus/battery_plus.dart'; // **إضافة جديدة:** للتعامل مع حالة البطارية
|
||||
|
||||
import 'package:sefer_driver/constant/table_names.dart';
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import '../home/captin/home_captain_controller.dart';
|
||||
import '../home/payment/captain_wallet_controller.dart';
|
||||
import 'battery_status.dart';
|
||||
import 'crud.dart';
|
||||
import 'encrypt_decrypt.dart';
|
||||
|
||||
/// LocationController - النسخة النهائية المتكاملة مع وضع توفير الطاقة
|
||||
///
|
||||
/// تم تصميم هذا المتحكم ليكون المحرك الجديد لإدارة الموقع في تطبيقك.
|
||||
/// يجمع بين الكفاءة العالية، المنطق الذكي، والتوافق الكامل مع بنية الكود الحالية.
|
||||
class LocationController extends GetxController {
|
||||
LocationData? _currentLocation;
|
||||
late Location location = Location();
|
||||
bool isLoading = false;
|
||||
late double heading = 0;
|
||||
late double previousTime = 0;
|
||||
late double latitude;
|
||||
late double totalDistance = 0;
|
||||
late double longitude;
|
||||
late DateTime time;
|
||||
late double speed = 0;
|
||||
bool isActive = false;
|
||||
late LatLng myLocation = LatLng(0, 0);
|
||||
String totalPoints = '0';
|
||||
LocationData? get currentLocation => _currentLocation;
|
||||
Timer? _locationTimer;
|
||||
// ===================================================================
|
||||
// ====== Tunables / المتغيرات القابلة للتعديل ======
|
||||
// ===================================================================
|
||||
// -- Normal Mode --
|
||||
static const double onMoveMetersNormal = 15.0;
|
||||
static const double offMoveMetersNormal = 200.0;
|
||||
static const Duration trackInsertEveryNormal = Duration(minutes: 1);
|
||||
static const Duration heartbeatEveryNormal = Duration(minutes: 2);
|
||||
|
||||
LatLng? _lastSavedPosition;
|
||||
// -- Power Saving Mode --
|
||||
static const double onMoveMetersPowerSave = 75.0;
|
||||
static const double offMoveMetersPowerSave = 500.0;
|
||||
static const Duration trackInsertEveryPowerSave = Duration(minutes: 2);
|
||||
static const Duration heartbeatEveryPowerSave = Duration(minutes: 5);
|
||||
|
||||
static const double lowWalletThreshold = -30000;
|
||||
static const int powerSaveTriggerLevel =
|
||||
20; // نسبة البطارية لتفعيل وضع التوفير
|
||||
static const int powerSaveExitLevel =
|
||||
25; // نسبة البطارية للخروج من وضع التوفير
|
||||
|
||||
// ===================================================================
|
||||
// ====== Services & Subscriptions (الخدمات والاشتراكات) ======
|
||||
// ===================================================================
|
||||
late final Location location = Location();
|
||||
final Battery _battery = Battery(); // **إضافة جديدة:** للتعامل مع البطارية
|
||||
StreamSubscription<LocationData>? _locSub;
|
||||
StreamSubscription<BatteryState>? _batterySub;
|
||||
Timer? _trackInsertTimer;
|
||||
Timer? _heartbeatTimer;
|
||||
|
||||
// ===================================================================
|
||||
// ====== Cached Controllers (لتجنب Get.find المتكرر) ======
|
||||
// ===================================================================
|
||||
late final HomeCaptainController _homeCtrl;
|
||||
late final CaptainWalletController _walletCtrl;
|
||||
|
||||
// ===================================================================
|
||||
// ====== Public state (لواجهة المستخدم والكلاسات الأخرى) ======
|
||||
// ===================================================================
|
||||
LatLng myLocation = const LatLng(0, 0);
|
||||
double heading = 0.0;
|
||||
double speed = 0.0;
|
||||
double totalDistance = 0.0;
|
||||
|
||||
// ===================================================================
|
||||
// ====== Internal state (للمنطق الداخلي) ======
|
||||
// ===================================================================
|
||||
bool _isReady = false;
|
||||
bool _isPowerSavingMode = false; // **إضافة جديدة:** لتتبع وضع توفير الطاقة
|
||||
LatLng? _lastSentLoc;
|
||||
String? _lastSentStatus;
|
||||
DateTime? _lastSentAt;
|
||||
LatLng? _lastPosForDistance;
|
||||
|
||||
// **إضافة جديدة:** متغيرات لحساب سلوك السائق
|
||||
LatLng? _lastSqlLoc;
|
||||
double? _lastSpeed;
|
||||
DateTime? _lastSpeedAt;
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
Future<void> onInit() async {
|
||||
super.onInit();
|
||||
location = Location();
|
||||
await location.changeSettings(
|
||||
accuracy: LocationAccuracy.high, interval: 5000, distanceFilter: 0);
|
||||
location.enableBackgroundMode(enable: true);
|
||||
await getLocation();
|
||||
await startLocationUpdates();
|
||||
print('LocationController onInit started...');
|
||||
|
||||
totalPoints = Get.put(CaptainWalletController()).totalPoints.toString();
|
||||
isActive = Get.put(HomeCaptainController()).isActive;
|
||||
}
|
||||
|
||||
String getLocationArea(double latitude, double longitude) {
|
||||
final locations = box.read(BoxName.locationName) ?? [];
|
||||
for (final location in locations) {
|
||||
final locationData = location as Map<String, dynamic>;
|
||||
final minLatitude =
|
||||
double.tryParse(locationData['min_latitude'].toString()) ?? 0.0;
|
||||
final maxLatitude =
|
||||
double.tryParse(locationData['max_latitude'].toString()) ?? 0.0;
|
||||
final minLongitude =
|
||||
double.tryParse(locationData['min_longitude'].toString()) ?? 0.0;
|
||||
final maxLongitude =
|
||||
double.tryParse(locationData['max_longitude'].toString()) ?? 0.0;
|
||||
|
||||
if (latitude >= minLatitude &&
|
||||
latitude <= maxLatitude &&
|
||||
longitude >= minLongitude &&
|
||||
longitude <= maxLongitude) {
|
||||
box.write(BoxName.serverChosen, (locationData['server_link']));
|
||||
return locationData['name'];
|
||||
}
|
||||
bool dependenciesReady = await _awaitDependencies();
|
||||
if (!dependenciesReady) {
|
||||
print(
|
||||
"❌ CRITICAL ERROR: Dependencies not found. Location services will not start.");
|
||||
return;
|
||||
}
|
||||
|
||||
box.write(BoxName.serverChosen, AppLink.seferCairoServer);
|
||||
return 'Cairo';
|
||||
_isReady = true;
|
||||
|
||||
await _initLocationSettings();
|
||||
await startLocationUpdates();
|
||||
_listenToBatteryChanges(); // **إضافة جديدة:** بدء الاستماع لتغيرات البطارية
|
||||
|
||||
print('✅ LocationController is ready and initialized.');
|
||||
}
|
||||
|
||||
int _insertCounter = 0;
|
||||
double? _lastSpeed;
|
||||
DateTime? _lastSpeedTime;
|
||||
@override
|
||||
void onClose() {
|
||||
print('🛑 Closing LocationController...');
|
||||
stopLocationUpdates();
|
||||
_batterySub?.cancel(); // إيقاف الاستماع للبطارية
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
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>();
|
||||
print("✅ Dependencies found and controllers are cached.");
|
||||
return true;
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
attempts++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// ====== Public Control Methods (دوال التحكم العامة) ======
|
||||
// ===================================================================
|
||||
|
||||
Future<void> startLocationUpdates() async {
|
||||
if (box.read(BoxName.driverID) != null) {
|
||||
_locationTimer =
|
||||
Timer.periodic(const Duration(seconds: 5), (timer) async {
|
||||
try {
|
||||
await BatteryNotifier.checkBatteryAndNotify();
|
||||
totalPoints =
|
||||
Get.find<CaptainWalletController>().totalPoints.toString();
|
||||
isActive = Get.find<HomeCaptainController>().isActive;
|
||||
|
||||
if (isActive && double.parse(totalPoints) > -30000) {
|
||||
await getLocation();
|
||||
if (myLocation.latitude == 0 && myLocation.longitude == 0) return;
|
||||
|
||||
String area =
|
||||
getLocationArea(myLocation.latitude, myLocation.longitude);
|
||||
|
||||
final payload = {
|
||||
'driver_id': box.read(BoxName.driverID).toString(),
|
||||
'latitude': myLocation.latitude.toString(),
|
||||
'longitude': myLocation.longitude.toString(),
|
||||
'heading': heading.toString(),
|
||||
'speed': (speed * 3.6).toStringAsFixed(1),
|
||||
'distance': totalDistance.toStringAsFixed(2),
|
||||
'status': box.read(BoxName.statusDriverLocation) ?? 'off',
|
||||
};
|
||||
|
||||
// ✅ تحديث للسيرفر
|
||||
await CRUD().post(
|
||||
link: '${AppLink.server}/ride/location/update.php',
|
||||
payload: payload,
|
||||
);
|
||||
|
||||
// ✅ تخزين في SQLite فقط إذا الرحلة On + تحرك أكثر من 10 متر
|
||||
if ((box.read(BoxName.statusDriverLocation) ?? 'off') == 'on') {
|
||||
if (_lastSavedPosition == null ||
|
||||
_calculateDistanceInMeters(_lastSavedPosition!, myLocation) >=
|
||||
10) {
|
||||
double currentSpeed = speed; // m/s
|
||||
double? acceleration = _calculateAcceleration(currentSpeed);
|
||||
|
||||
await sql.insertData({
|
||||
'driver_id': box.read(BoxName.driverID).toString(),
|
||||
'latitude': myLocation.latitude,
|
||||
'longitude': myLocation.longitude,
|
||||
'acceleration': acceleration ?? 0.0,
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
}, TableName.behavior);
|
||||
|
||||
_lastSavedPosition = myLocation;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ إدخال للسيرفر كل دقيقة
|
||||
_insertCounter++;
|
||||
// Log.print('_insertCounter: ${_insertCounter}');
|
||||
if (_insertCounter == 12) {
|
||||
_insertCounter = 0;
|
||||
await CRUD().post(
|
||||
link: '${AppLink.server}/ride/location/add.php',
|
||||
payload: payload,
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ تحديث الكاميرا
|
||||
Get.find<HomeCaptainController>()
|
||||
.mapHomeCaptainController
|
||||
?.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(
|
||||
bearing: Get.find<LocationController>().heading,
|
||||
target: myLocation,
|
||||
zoom: 17, // Adjust zoom level as needed
|
||||
),
|
||||
),
|
||||
);
|
||||
// if (Get.isRegistered()) {
|
||||
// Get.find<MapDriverController>().mapController?.animateCamera(
|
||||
// CameraUpdate.newCameraPosition(
|
||||
// CameraPosition(
|
||||
// bearing: Get.find<LocationController>().heading,
|
||||
// target: myLocation,
|
||||
// zoom: 17, // Adjust zoom level as needed
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
}
|
||||
} catch (e) {
|
||||
print('Location update error: $e');
|
||||
}
|
||||
});
|
||||
// ... (الكود لم يتغير)
|
||||
if (!_isReady) {
|
||||
print("Cannot start updates: LocationController is not ready.");
|
||||
return;
|
||||
}
|
||||
final points = _walletCtrl.totalPoints;
|
||||
if (double.parse(points) <= lowWalletThreshold) {
|
||||
print('❌ Blocked: low wallet balance ($points)');
|
||||
stopLocationUpdates();
|
||||
return;
|
||||
}
|
||||
if (_locSub != null) {
|
||||
print('Location updates are already active.');
|
||||
return;
|
||||
}
|
||||
if (await _ensureServiceAndPermission()) {
|
||||
_subscribeLocationStream();
|
||||
_startTimers();
|
||||
}
|
||||
}
|
||||
|
||||
void stopLocationUpdates() {
|
||||
_locationTimer?.cancel();
|
||||
// ... (الكود لم يتغير)
|
||||
_locSub?.cancel();
|
||||
_locSub = null;
|
||||
_trackInsertTimer?.cancel();
|
||||
_trackInsertTimer = null;
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = null;
|
||||
print('Location updates and timers stopped.');
|
||||
}
|
||||
|
||||
Future<void> getLocation() async {
|
||||
Future<LocationData?> getLocation() async {
|
||||
// ... (الكود لم يتغير)
|
||||
try {
|
||||
if (await _ensureServiceAndPermission()) {
|
||||
return await location.getLocation();
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ FAILED to get single location: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// ====== Core Logic (المنطق الأساسي) ======
|
||||
// ===================================================================
|
||||
|
||||
Future<void> _initLocationSettings() async {
|
||||
// ... (الكود لم يتغير)
|
||||
await location.changeSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
interval: 5000,
|
||||
distanceFilter: 0,
|
||||
);
|
||||
await location.enableBackgroundMode(enable: true);
|
||||
}
|
||||
|
||||
Future<bool> _ensureServiceAndPermission() async {
|
||||
// ... (الكود لم يتغير)
|
||||
bool serviceEnabled = await location.serviceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
serviceEnabled = await location.requestService();
|
||||
if (!serviceEnabled) return;
|
||||
if (!serviceEnabled) return false;
|
||||
}
|
||||
|
||||
PermissionStatus permissionGranted = await location.hasPermission();
|
||||
if (permissionGranted == PermissionStatus.denied) {
|
||||
permissionGranted = await location.requestPermission();
|
||||
if (permissionGranted != PermissionStatus.granted) return;
|
||||
var perm = await location.hasPermission();
|
||||
if (perm == PermissionStatus.denied) {
|
||||
perm = await location.requestPermission();
|
||||
if (perm != PermissionStatus.granted) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future.delayed(Duration(milliseconds: 500), () async {
|
||||
try {
|
||||
LocationData _locationData = await location.getLocation();
|
||||
if (_locationData.latitude != null && _locationData.longitude != null) {
|
||||
myLocation =
|
||||
LatLng(_locationData.latitude!, _locationData.longitude!);
|
||||
} else {
|
||||
myLocation = LatLng(0, 0);
|
||||
void _subscribeLocationStream() {
|
||||
_locSub?.cancel();
|
||||
_locSub = location.onLocationChanged.listen(
|
||||
(loc) async {
|
||||
if (!_isReady) return;
|
||||
try {
|
||||
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 = _haversineMeters(_lastPosForDistance!, pos);
|
||||
if (d > 2.0) totalDistance += d;
|
||||
}
|
||||
_lastPosForDistance = pos;
|
||||
// ✅ تحديث الكاميرا
|
||||
_homeCtrl.mapHomeCaptainController?.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(
|
||||
bearing: Get.find<LocationController>().heading,
|
||||
target: myLocation,
|
||||
zoom: 17, // Adjust zoom level as needed
|
||||
),
|
||||
),
|
||||
);
|
||||
update(); // تحديث الواجهة الرسومية بالبيانات الجديدة
|
||||
|
||||
await _smartSend(pos, loc);
|
||||
|
||||
// **إضافة جديدة:** حفظ سلوك السائق في قاعدة البيانات المحلية
|
||||
await _saveBehaviorIfMoved(pos, now, currentSpeed: speed);
|
||||
} catch (e) {
|
||||
print('Error in onLocationChanged: $e');
|
||||
}
|
||||
},
|
||||
onError: (e) => print('Location stream error: $e'),
|
||||
);
|
||||
print('📡 Subscribed to location stream.');
|
||||
}
|
||||
|
||||
speed = _locationData.speed ?? 0;
|
||||
heading = _locationData.heading ?? 0;
|
||||
void _startTimers() {
|
||||
_trackInsertTimer?.cancel();
|
||||
_heartbeatTimer?.cancel();
|
||||
|
||||
update();
|
||||
} catch (e) {
|
||||
print("Error getting location: $e");
|
||||
final trackDuration =
|
||||
_isPowerSavingMode ? trackInsertEveryPowerSave : trackInsertEveryNormal;
|
||||
final heartbeatDuration =
|
||||
_isPowerSavingMode ? heartbeatEveryPowerSave : heartbeatEveryNormal;
|
||||
|
||||
_trackInsertTimer =
|
||||
Timer.periodic(trackDuration, (_) => _addSingleTrackPoint());
|
||||
_heartbeatTimer =
|
||||
Timer.periodic(heartbeatDuration, (_) => _sendStationaryHeartbeat());
|
||||
|
||||
print('⏱️ Background timers started (Power Save: $_isPowerSavingMode).');
|
||||
}
|
||||
|
||||
Future<void> _smartSend(LatLng pos, LocationData loc) async {
|
||||
final String driverStatus = box.read(BoxName.statusDriverLocation) ?? 'off';
|
||||
final distSinceSent =
|
||||
(_lastSentLoc == null) ? 999.0 : _haversineMeters(_lastSentLoc!, pos);
|
||||
|
||||
final onMoveThreshold =
|
||||
_isPowerSavingMode ? onMoveMetersPowerSave : onMoveMetersNormal;
|
||||
final offMoveThreshold =
|
||||
_isPowerSavingMode ? offMoveMetersPowerSave : offMoveMetersNormal;
|
||||
|
||||
bool shouldSend = false;
|
||||
|
||||
if (driverStatus != _lastSentStatus) {
|
||||
shouldSend = true;
|
||||
if (driverStatus == 'on') {
|
||||
totalDistance = 0.0;
|
||||
_lastPosForDistance = pos;
|
||||
}
|
||||
print(
|
||||
'Status changed: ${_lastSentStatus ?? '-'} -> $driverStatus. Sending...');
|
||||
} else if (driverStatus == 'on') {
|
||||
if (distSinceSent >= onMoveThreshold) {
|
||||
shouldSend = true;
|
||||
}
|
||||
} else {
|
||||
// driverStatus == 'off'
|
||||
if (distSinceSent >= offMoveThreshold) {
|
||||
shouldSend = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldSend) return;
|
||||
|
||||
await _sendUpdate(pos, driverStatus, loc);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// ====== Battery Logic (منطق البطارية) ======
|
||||
// ===================================================================
|
||||
|
||||
void _listenToBatteryChanges() async {
|
||||
_checkBatteryLevel(await _battery.batteryLevel);
|
||||
_batterySub =
|
||||
_battery.onBatteryStateChanged.listen((BatteryState state) async {
|
||||
_checkBatteryLevel(await _battery.batteryLevel);
|
||||
});
|
||||
}
|
||||
|
||||
double _calculateDistanceInMeters(LatLng start, LatLng end) {
|
||||
const p = 0.017453292519943295;
|
||||
final a = 0.5 -
|
||||
cos((end.latitude - start.latitude) * p) / 2 +
|
||||
cos(start.latitude * p) *
|
||||
cos(end.latitude * p) *
|
||||
(1 - cos((end.longitude - start.longitude) * p)) /
|
||||
2;
|
||||
return 12742 * 1000 * asin(sqrt(a)); // meters
|
||||
}
|
||||
void _checkBatteryLevel(int level) {
|
||||
final bool wasInPowerSaveMode = _isPowerSavingMode;
|
||||
|
||||
double? _calculateAcceleration(double currentSpeed) {
|
||||
final now = DateTime.now();
|
||||
if (_lastSpeed != null && _lastSpeedTime != null) {
|
||||
final deltaTime =
|
||||
now.difference(_lastSpeedTime!).inMilliseconds / 1000.0; // seconds
|
||||
if (deltaTime > 0) {
|
||||
final acceleration = (currentSpeed - _lastSpeed!) / deltaTime;
|
||||
_lastSpeed = currentSpeed;
|
||||
_lastSpeedTime = now;
|
||||
return double.parse(acceleration.toStringAsFixed(2));
|
||||
}
|
||||
if (level <= powerSaveTriggerLevel) {
|
||||
_isPowerSavingMode = true;
|
||||
} else if (level >= powerSaveExitLevel) {
|
||||
_isPowerSavingMode = false;
|
||||
}
|
||||
|
||||
if (_isPowerSavingMode != wasInPowerSaveMode) {
|
||||
if (_isPowerSavingMode) {
|
||||
Get.snackbar(
|
||||
"وضع توفير الطاقة مُفعّل",
|
||||
"البطارية منخفضة. سنقلل تحديثات الموقع للحفاظ على طاقتك.",
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Get.theme.primaryColor.withOpacity(0.9),
|
||||
colorText: Get.theme.colorScheme.onPrimary,
|
||||
duration: const Duration(seconds: 7),
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"العودة للوضع الطبيعي",
|
||||
"تم شحن البطارية. عادت تحديثات الموقع لوضعها الطبيعي.",
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Get.theme.colorScheme.secondary.withOpacity(0.9),
|
||||
colorText: Get.theme.colorScheme.onSecondary,
|
||||
duration: const Duration(seconds: 5),
|
||||
);
|
||||
}
|
||||
_startTimers();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// ====== API Communication & Helpers (التواصل مع السيرفر والدوال المساعدة) ======
|
||||
// ===================================================================
|
||||
|
||||
Future<void> _sendUpdate(LatLng pos, String status, LocationData loc) async {
|
||||
final payload = _buildPayload(pos, status, loc);
|
||||
try {
|
||||
await CRUD().post(
|
||||
link: '${AppLink.server}/ride/location/update.php',
|
||||
payload: payload,
|
||||
);
|
||||
_lastSentLoc = pos;
|
||||
_lastSentStatus = status;
|
||||
_lastSentAt = DateTime.now();
|
||||
print('✅ Sent to update.php [$status]');
|
||||
} catch (e) {
|
||||
print('❌ FAILED to send to update.php: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addSingleTrackPoint() async {
|
||||
if (!_isReady) return;
|
||||
|
||||
final String driverStatus =
|
||||
(box.read(BoxName.statusDriverLocation) ?? 'off').toString();
|
||||
|
||||
if (myLocation.latitude == 0 && myLocation.longitude == 0) return;
|
||||
if (_lastSentLoc == null) return; // حماية إضافية
|
||||
|
||||
// قيَم رقمية آمنة
|
||||
final double safeHeading =
|
||||
(heading is num) ? (heading as num).toDouble() : 0.0;
|
||||
final double safeSpeed = (speed is num) ? (speed as num).toDouble() : 0.0;
|
||||
final double safeDistKm = (totalDistance is num)
|
||||
? (totalDistance as num).toDouble() / 1000.0
|
||||
: 0.0;
|
||||
|
||||
final String driverId =
|
||||
(box.read(BoxName.driverID) ?? '').toString().trim();
|
||||
if (driverId.isEmpty) return; // لا ترسل بدون DriverID
|
||||
|
||||
// ✅ كل شيء Strings فقط
|
||||
final Map<String, String> payload = {
|
||||
'driver_id': driverId,
|
||||
'latitude': _lastSentLoc!.latitude.toStringAsFixed(6),
|
||||
'longitude': _lastSentLoc!.longitude.toStringAsFixed(6),
|
||||
'heading': safeHeading.toStringAsFixed(1),
|
||||
'speed': (safeSpeed < 0.5 ? 0.0 : safeSpeed)
|
||||
.toString(), // أو toStringAsFixed(2)
|
||||
'distance': safeDistKm.toStringAsFixed(2),
|
||||
'status': driverStatus,
|
||||
'carType': (box.read(BoxName.carType) ?? 'default').toString(),
|
||||
};
|
||||
|
||||
try {
|
||||
print('⏱️ Adding a single point to car_track... $payload');
|
||||
await CRUD().post(
|
||||
link: '${AppLink.server}/ride/location/add.php',
|
||||
payload: payload, // ← الآن Map<String,String>
|
||||
);
|
||||
} catch (e) {
|
||||
print('❌ FAILED to send single track point: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendStationaryHeartbeat() async {
|
||||
if (!_isReady) return;
|
||||
if (_lastSentLoc == null || _lastSentAt == null) return;
|
||||
if (DateTime.now().difference(_lastSentAt!).inSeconds < 90) return;
|
||||
final distSinceSent = _haversineMeters(_lastSentLoc!, myLocation);
|
||||
if (distSinceSent >= onMoveMetersNormal) return;
|
||||
print('🫀 Driver is stationary, sending heartbeat...');
|
||||
|
||||
final String driverStatus =
|
||||
(box.read(BoxName.statusDriverLocation) ?? 'off').toString();
|
||||
|
||||
// ✅ كل شيء Strings
|
||||
final Map<String, String> payload = {
|
||||
'driver_id': (box.read(BoxName.driverID) ?? '').toString(),
|
||||
'latitude': _lastSentLoc!.latitude.toStringAsFixed(6),
|
||||
'longitude': _lastSentLoc!.longitude.toStringAsFixed(6),
|
||||
'heading': heading.toStringAsFixed(1),
|
||||
// ملاحظة: هنا السرعة تبقى بالمتر/ث بعد التحويل أدناه؛ وحّدتّها لكتابة String
|
||||
'speed': ((speed < 0.5) ? 0.0 : speed).toString(),
|
||||
'distance': (totalDistance / 1000).toStringAsFixed(2),
|
||||
'status': driverStatus,
|
||||
'carType': (box.read(BoxName.carType) ?? 'default').toString(),
|
||||
// 'hb': '1'
|
||||
};
|
||||
|
||||
try {
|
||||
await CRUD().post(
|
||||
link: '${AppLink.server}/ride/location/update.php',
|
||||
payload: payload,
|
||||
);
|
||||
_lastSentAt = DateTime.now();
|
||||
} catch (e) {
|
||||
print('❌ FAILED to send Heartbeat: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> _buildPayload(
|
||||
LatLng pos, String status, LocationData loc) {
|
||||
return {
|
||||
'driver_id': (box.read(BoxName.driverID) ?? '').toString(),
|
||||
'latitude': pos.latitude.toStringAsFixed(6),
|
||||
'longitude': pos.longitude.toStringAsFixed(6),
|
||||
'heading': (loc.heading ?? heading).toStringAsFixed(1),
|
||||
// هنا أنت بتحوّل السرعة إلى كم/س (×3.6) — ممتاز
|
||||
'speed': ((loc.speed ?? speed) * 3.6).toStringAsFixed(1),
|
||||
'status': status,
|
||||
'distance': (totalDistance / 1000).toStringAsFixed(2),
|
||||
'carType': (box.read(BoxName.carType) ?? 'default').toString(), // 👈
|
||||
};
|
||||
}
|
||||
|
||||
double _haversineMeters(LatLng a, LatLng b) {
|
||||
const p = 0.017453292519943295;
|
||||
final h = 0.5 -
|
||||
cos((b.latitude - a.latitude) * p) / 2 +
|
||||
cos(a.latitude * p) *
|
||||
cos(b.latitude * p) *
|
||||
(1 - cos((b.longitude - a.longitude) * p)) /
|
||||
2;
|
||||
return 12742 * 1000 * asin(sqrt(h));
|
||||
}
|
||||
|
||||
// **إضافة جديدة:** دوال لحفظ سلوك السائق محلياً
|
||||
|
||||
/// يحسب التسارع بالمتر/ثانية^2
|
||||
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;
|
||||
_lastSpeedTime = now;
|
||||
_lastSpeedAt = now;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// يحفظ سلوك السائق (الموقع، التسارع) في قاعدة بيانات SQLite المحلية
|
||||
Future<void> _saveBehaviorIfMoved(LatLng pos, DateTime now,
|
||||
{required double currentSpeed}) async {
|
||||
final dist =
|
||||
(_lastSqlLoc == null) ? 999.0 : _haversineMeters(_lastSqlLoc!, pos);
|
||||
if (dist < 15.0) return; // الحفظ فقط عند التحرك لمسافة 15 متر على الأقل
|
||||
|
||||
final accel = _calcAcceleration(currentSpeed, now) ?? 0.0;
|
||||
try {
|
||||
final now = DateTime.now();
|
||||
|
||||
final double lat =
|
||||
double.parse(pos.latitude.toStringAsFixed(6)); // دقة 6 أرقام
|
||||
final double lon =
|
||||
double.parse(pos.longitude.toStringAsFixed(6)); // دقة 6 أرقام
|
||||
final double acc = double.parse(
|
||||
(accel is num ? accel as num : 0).toStringAsFixed(2)); // دقة منزلتين
|
||||
|
||||
await sql.insertData({
|
||||
'driver_id': (box.read(BoxName.driverID) ?? '').toString(), // TEXT
|
||||
'latitude': lat, // REAL
|
||||
'longitude': lon, // REAL
|
||||
'acceleration': acc, // REAL
|
||||
'created_at': now.toIso8601String(), // TEXT
|
||||
'updated_at': now.toIso8601String(), // TEXT
|
||||
}, TableName.behavior);
|
||||
_lastSqlLoc = pos;
|
||||
} catch (e) {
|
||||
print('❌ FAILED to insert to SQLite (behavior): $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ class DeviceHelper {
|
||||
|
||||
// Extract relevant device information
|
||||
final String deviceId = Platform.isAndroid
|
||||
? deviceData['androidId'] ?? deviceData['serialNumber'] ?? 'unknown'
|
||||
? deviceData['androidId'] ?? deviceData['fingerprint'] ?? 'unknown'
|
||||
: deviceData['identifierForVendor'] ?? 'unknown';
|
||||
|
||||
final String deviceModel = deviceData['model'] ?? 'unknown';
|
||||
@@ -199,6 +199,7 @@ class DeviceHelper {
|
||||
|
||||
// Generate and return the encrypted fingerprint
|
||||
final String fingerprint = '${deviceId}_${deviceModel}_$osVersion';
|
||||
Log.print('fingerprint: ${fingerprint}');
|
||||
// print(EncryptionHelper.instance.encryptData(fingerprint));
|
||||
return (fingerprint);
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user