635 lines
21 KiB
Dart
Executable File
635 lines
21 KiB
Dart
Executable File
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:geolocator/geolocator.dart' as geo;
|
|
import 'package:google_maps_flutter/google_maps_flutter.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 '../../constant/box_name.dart';
|
|
import '../../constant/links.dart';
|
|
import '../../main.dart';
|
|
import '../home/captin/home_captain_controller.dart';
|
|
import '../home/captin/map_driver_controller.dart';
|
|
import '../home/payment/captain_wallet_controller.dart';
|
|
import 'background_service.dart';
|
|
import 'crud.dart';
|
|
|
|
class LocationController extends GetxController with WidgetsBindingObserver {
|
|
// ===================================================================
|
|
// ====== Tunables ======
|
|
// ===================================================================
|
|
static const Duration recordIntervalNormal = Duration(seconds: 3);
|
|
static const Duration uploadBatchIntervalNormal = Duration(minutes: 2);
|
|
static const Duration recordIntervalPowerSave = Duration(seconds: 10);
|
|
static const Duration uploadBatchIntervalPowerSave = Duration(minutes: 5);
|
|
|
|
static const double lowWalletThreshold = -200;
|
|
static const int powerSaveTriggerLevel = 20;
|
|
static const int powerSaveExitLevel = 25;
|
|
|
|
// ===================================================================
|
|
// ====== Services & Variables ======
|
|
// ===================================================================
|
|
late final Location location = Location();
|
|
final Battery _battery = Battery();
|
|
|
|
IO.Socket? socket;
|
|
bool isSocketConnected = false;
|
|
Timer? _socketHeartbeat;
|
|
|
|
StreamSubscription<LocationData>? _locSub;
|
|
StreamSubscription<BatteryState>? _batterySub;
|
|
|
|
Timer? _recordTimer;
|
|
Timer? _uploadBatchTimer;
|
|
|
|
late final HomeCaptainController _homeCtrl;
|
|
late final CaptainWalletController _walletCtrl;
|
|
|
|
LatLng myLocation = const LatLng(0, 0);
|
|
double heading = 0.0;
|
|
double speed = 0.0;
|
|
double totalDistance = 0.0;
|
|
bool _isReady = false;
|
|
bool _isPowerSavingMode = false;
|
|
|
|
final List<Map<String, dynamic>> _trackBuffer = [];
|
|
|
|
LatLng? _lastPosForDistance;
|
|
LatLng? _lastRecordedRealLoc;
|
|
DateTime? _lastRecordedTime;
|
|
|
|
LatLng? _lastSqlLoc;
|
|
double? _lastSpeed;
|
|
DateTime? _lastSpeedAt;
|
|
|
|
@override
|
|
Future<void> onInit() async {
|
|
super.onInit();
|
|
print('🚀 LocationController Starting...');
|
|
|
|
// 1. Register Lifecycle Observer
|
|
WidgetsBinding.instance.addObserver(this);
|
|
box.write(BoxName.isAppInForeground, true);
|
|
|
|
// مراقب الحالة (Status Watcher)
|
|
box.listenKey(BoxName.statusDriverLocation, (value) {
|
|
if (value == 'blocked') {
|
|
print("⛔ Driver is Blocked: Force Stopping Location Updates.");
|
|
stopLocationUpdates();
|
|
if (socket != null && socket!.connected) {
|
|
socket!.emit('update_location', {
|
|
'driver_id': box.read(BoxName.driverID),
|
|
'status': 'blocked',
|
|
'lat': myLocation.latitude,
|
|
'lng': myLocation.longitude,
|
|
'heading': heading,
|
|
'speed': speed * 3.6,
|
|
'distance': totalDistance
|
|
});
|
|
socket!.disconnect();
|
|
}
|
|
}
|
|
});
|
|
|
|
bool deps = await _awaitDependencies();
|
|
if (!deps) return;
|
|
|
|
_isReady = true;
|
|
|
|
initSocket();
|
|
await _initLocationSettings();
|
|
_listenToBatteryChanges();
|
|
|
|
if (box.read(BoxName.statusDriverLocation) != 'blocked') {
|
|
await startLocationUpdates();
|
|
}
|
|
|
|
print('✅ LocationController Initialized.');
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
box.write(BoxName.isAppInForeground, false);
|
|
stopLocationUpdates();
|
|
_batterySub?.cancel();
|
|
_stopHeartbeat();
|
|
socket?.dispose();
|
|
super.onClose();
|
|
}
|
|
|
|
// ===================================================================
|
|
// 🔥 Lifecycle Manager (Fixes Freeze & Background issues)
|
|
// ===================================================================
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
if (state == AppLifecycleState.resumed) {
|
|
print("📱 Lifecycle: App is in FOREGROUND");
|
|
box.write(BoxName.isAppInForeground, true);
|
|
|
|
// إيقاف خدمة الخلفية لأننا في الواجهة الآن
|
|
BackgroundServiceHelper.stopService();
|
|
|
|
// التأكد من أن السوكيت متصل، وإذا لا، نعيد الاتصال فوراً
|
|
if (socket == null || !socket!.connected) {
|
|
print("🔄 Socket disconnected in background. Reconnecting now...");
|
|
initSocket();
|
|
} else {
|
|
// إذا كان متصلاً، ننعش المستمعين فقط للتأكد
|
|
_setupSocketListeners();
|
|
}
|
|
} else if (state == AppLifecycleState.paused ||
|
|
state == AppLifecycleState.detached) {
|
|
print("📱 Lifecycle: App is in BACKGROUND");
|
|
box.write(BoxName.isAppInForeground, false);
|
|
|
|
// تشغيل خدمة الخلفية لضمان بقاء التطبيق حياً
|
|
BackgroundServiceHelper.startService();
|
|
|
|
// ملاحظة: لا نقطع السوكيت هنا، نتركه يعمل قدر الإمكان
|
|
}
|
|
}
|
|
|
|
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>();
|
|
return true;
|
|
}
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
attempts++;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ===================================================================
|
|
// ====== Socket Logic (Improved) ======
|
|
// ===================================================================
|
|
|
|
void initSocket() {
|
|
String driverId = box.read(BoxName.driverID).toString();
|
|
String token = box.read(BoxName.tokenDriver).toString();
|
|
String platform = Platform.isIOS ? 'ios' : 'android';
|
|
|
|
// 1. إذا كان السوكيت موجوداً، فقط تأكد من اتصاله
|
|
if (socket != null) {
|
|
if (!socket!.connected) {
|
|
print("🟡 Socket exists but disconnected. Reconnecting...");
|
|
socket!.connect();
|
|
}
|
|
_setupSocketListeners(); // تحديث المستمعين
|
|
return;
|
|
}
|
|
|
|
print("🟡 [LocationController] Creating 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());
|
|
|
|
socket!.connect();
|
|
_setupSocketListeners();
|
|
}
|
|
|
|
// دالة منفصلة لضمان عدم تكرار المستمعين
|
|
void _setupSocketListeners() {
|
|
if (socket == null) return;
|
|
|
|
// تنظيف القديم أولاً
|
|
socket!.off('connect');
|
|
socket!.off('disconnect');
|
|
socket!.off('new_ride_request');
|
|
socket!.off('ride_cancelled');
|
|
|
|
socket!.onConnect((_) {
|
|
print('✅ Socket Connected! ID: ${socket?.id}');
|
|
isSocketConnected = true;
|
|
_startHeartbeat();
|
|
});
|
|
|
|
socket!.onDisconnect((_) {
|
|
print('❌ Socket Disconnected');
|
|
isSocketConnected = false;
|
|
_stopHeartbeat();
|
|
});
|
|
|
|
// 🔥 الاستماع للطلبات الجديدة
|
|
socket!.on('new_ride_request', (data) {
|
|
print("🔔 Socket: New Ride Request Arrived!");
|
|
|
|
// نستخدم Future.microtask لضمان عدم حظر الـ UI Thread
|
|
Future.microtask(() {
|
|
if (data != null) {
|
|
try {
|
|
List<dynamic> rawList = [];
|
|
if (data is String) {
|
|
var decoded = jsonDecode(data);
|
|
if (decoded is List) rawList = decoded;
|
|
} else if (data is List) {
|
|
if (data.isNotEmpty) {
|
|
rawList = (data[0] is List) ? data[0] : data;
|
|
}
|
|
}
|
|
|
|
if (rawList.isNotEmpty) {
|
|
Map<String, dynamic> convertedMap = {};
|
|
for (int i = 0; i < rawList.length; i++) {
|
|
convertedMap[i.toString()] = rawList[i];
|
|
}
|
|
handleIncomingOrder(convertedMap, "Socket");
|
|
}
|
|
} catch (e) {
|
|
print("❌ Error processing socket data: $e");
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// 🔥 الاستماع للإلغاء
|
|
socket!.on('cancel_ride', (data) {
|
|
print("🚫 Socket: Ride Cancelled Event Received");
|
|
String reason = data['reason'] ?? 'No reason provided';
|
|
if (Get.isRegistered<MapDriverController>()) {
|
|
Get.find<MapDriverController>()
|
|
.processRideCancelledByPassenger(reason, source: "Socket");
|
|
}
|
|
});
|
|
}
|
|
|
|
// داخل LocationController
|
|
|
|
Future<void> handleIncomingOrder(
|
|
Map<String, dynamic> rideData, String source) async {
|
|
print("📦 Socket Order Received from ($source)");
|
|
|
|
try {
|
|
// 1. التحقق من صحة البيانات
|
|
if (rideData.isEmpty || !rideData.containsKey('16')) {
|
|
print("❌ Socket Error: Invalid Ride Data.");
|
|
return;
|
|
}
|
|
|
|
// 2. تجهيز البيانات (DriverList)
|
|
List<dynamic> driverList = [];
|
|
if (rideData.length > 0) {
|
|
var sortedKeys = rideData.keys.map((e) => int.tryParse(e) ?? 0).toList()
|
|
..sort();
|
|
for (var key in sortedKeys) {
|
|
driverList.add(rideData[key.toString()]);
|
|
}
|
|
}
|
|
|
|
// 3. التنقل (باستخدام الاسم لضمان عمل الشرط)
|
|
try {
|
|
if (await FlutterOverlayWindow.isActive()) {
|
|
print("📲 Closing Overlay because App took control via Socket");
|
|
await FlutterOverlayWindow.closeOverlay();
|
|
}
|
|
} catch (e) {
|
|
print("Overlay check error: $e");
|
|
}
|
|
// ✅ هذا الشرط سيعمل الآن بدقة لأننا سنستخدم toNamed
|
|
if (Get.currentRoute != '/OrderRequestPage') {
|
|
print("🚀 Socket: Navigating to OrderRequestPage...");
|
|
|
|
// 🔥 التعديل هنا: استخدمنا Get.toNamed بدلاً من Get.to
|
|
// هذا يضمن تطابق الاسم مع ما هو موجود في main.dart
|
|
Get.toNamed('/OrderRequestPage', arguments: {
|
|
'myListString': jsonEncode(driverList),
|
|
'DriverList': driverList,
|
|
'body': 'New Trip Request via Socket ⚡'
|
|
});
|
|
} else {
|
|
print("⚠️ User is already on OrderRequestPage. Skipping navigation.");
|
|
}
|
|
} catch (e) {
|
|
print("❌ Socket Navigation Error: $e");
|
|
}
|
|
}
|
|
|
|
void _startHeartbeat() {
|
|
_socketHeartbeat?.cancel();
|
|
_socketHeartbeat = Timer.periodic(const Duration(seconds: 25), (timer) {
|
|
if (socket != null && isSocketConnected && myLocation.latitude != 0) {
|
|
emitLocationToSocket(myLocation, heading, speed);
|
|
}
|
|
});
|
|
}
|
|
|
|
void _stopHeartbeat() {
|
|
_socketHeartbeat?.cancel();
|
|
}
|
|
|
|
// In LocationController.dart
|
|
|
|
void emitLocationToSocket(LatLng pos, double head, double spd) {
|
|
String status = box.read(BoxName.statusDriverLocation) ?? 'on';
|
|
|
|
// Basic payload
|
|
var payload = {
|
|
'driver_id': box.read(BoxName.driverID),
|
|
'lat': pos.latitude,
|
|
'lng': pos.longitude,
|
|
'heading': head,
|
|
'speed': spd * 3.6,
|
|
'status': status,
|
|
'distance': totalDistance,
|
|
};
|
|
|
|
// 🔥 CRITICAL FIX: Inject Passenger ID if a ride is active 🔥
|
|
if (Get.isRegistered<MapDriverController>()) {
|
|
final mapCtrl = Get.find<MapDriverController>();
|
|
|
|
// Check if ride is started/active and we have a passenger ID
|
|
if (mapCtrl.isRideStarted && mapCtrl.passengerId != null) {
|
|
payload['passenger_id'] =
|
|
mapCtrl.passengerId; // This triggers the PHP forwarding
|
|
payload['ride_id'] = mapCtrl.rideId; // Good for debugging
|
|
}
|
|
}
|
|
|
|
// Debug print to verify
|
|
// print('🚀 Emitting Location: $payload');
|
|
|
|
if (socket != null && socket!.connected) {
|
|
socket!.emit('update_location', payload);
|
|
}
|
|
}
|
|
// ===================================================================
|
|
// ====== Tracking Logic ======
|
|
// ===================================================================
|
|
|
|
Future<void> startLocationUpdates() async {
|
|
_isReady = true;
|
|
String currentStatus = box.read(BoxName.statusDriverLocation) ?? 'off';
|
|
if (currentStatus == 'blocked') {
|
|
stopLocationUpdates();
|
|
return;
|
|
}
|
|
|
|
// Start background service
|
|
await BackgroundServiceHelper.startService();
|
|
|
|
if (socket == null || !socket!.connected) {
|
|
initSocket();
|
|
}
|
|
|
|
if (_locSub != null) return;
|
|
|
|
if (await _ensureServiceAndPermission()) {
|
|
_subscribeLocationStream();
|
|
_startBatchTimers();
|
|
}
|
|
}
|
|
|
|
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,
|
|
);
|
|
|
|
_locSub = location.onLocationChanged.listen((LocationData loc) async {
|
|
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 = _calculateDistance(_lastPosForDistance!, pos);
|
|
if (d > 5.0) totalDistance += d;
|
|
}
|
|
_lastPosForDistance = pos;
|
|
|
|
update();
|
|
emitLocationToSocket(pos, heading, speed);
|
|
await _saveBehaviorIfMoved(pos, now, currentSpeed: speed);
|
|
}, onError: (e) => print('❌ Location Stream Error: $e'));
|
|
}
|
|
|
|
Future<void> stopLocationUpdates() async {
|
|
print("🛑 Stopping Location Updates...");
|
|
|
|
_locSub?.cancel();
|
|
_locSub = null;
|
|
_recordTimer?.cancel();
|
|
_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();
|
|
}
|
|
|
|
await BackgroundServiceHelper.stopService();
|
|
socket = null;
|
|
isSocketConnected = false;
|
|
_isReady = false;
|
|
}
|
|
|
|
// ===================================================================
|
|
// ====== Batch Logic & Helpers ======
|
|
// ===================================================================
|
|
|
|
void _startBatchTimers() {
|
|
_recordTimer?.cancel();
|
|
_uploadBatchTimer?.cancel();
|
|
|
|
final recDur =
|
|
_isPowerSavingMode ? recordIntervalPowerSave : recordIntervalNormal;
|
|
final upDur = _isPowerSavingMode
|
|
? uploadBatchIntervalPowerSave
|
|
: uploadBatchIntervalNormal;
|
|
|
|
_recordTimer =
|
|
Timer.periodic(recDur, (_) => _recordCurrentLocationToBuffer());
|
|
_uploadBatchTimer = Timer.periodic(upDur, (_) => _flushBufferToServer());
|
|
}
|
|
|
|
void _recordCurrentLocationToBuffer() {
|
|
if (myLocation.latitude == 0) return;
|
|
final now = DateTime.now();
|
|
double distFromLast = 0.0;
|
|
if (_lastRecordedRealLoc != null) {
|
|
distFromLast = _calculateDistance(_lastRecordedRealLoc!, myLocation);
|
|
}
|
|
bool moved = distFromLast > 10.0;
|
|
bool timeForced = _lastRecordedTime == null ||
|
|
now.difference(_lastRecordedTime!).inSeconds >= 60;
|
|
|
|
if ((moved && speed > 0.5) || timeForced) {
|
|
_lastRecordedRealLoc = myLocation;
|
|
_lastRecordedTime = now;
|
|
final point = {
|
|
'lat': double.parse(myLocation.latitude.toStringAsFixed(6)),
|
|
'lng': double.parse(myLocation.longitude.toStringAsFixed(6)),
|
|
'spd': double.parse((speed * 3.6).toStringAsFixed(1)),
|
|
'head': int.parse(heading.toStringAsFixed(0)),
|
|
'st': box.read(BoxName.statusDriverLocation) ?? 'off',
|
|
'ts': now.toIso8601String(),
|
|
};
|
|
_trackBuffer.add(point);
|
|
}
|
|
}
|
|
|
|
Future<void> _flushBufferToServer() async {
|
|
if (_trackBuffer.isEmpty) return;
|
|
List<Map<String, dynamic>> batch = List.from(_trackBuffer);
|
|
_trackBuffer.clear();
|
|
final String driverId = (box.read(BoxName.driverID) ?? '').toString();
|
|
try {
|
|
await CRUD().post(
|
|
link: '${AppLink.locationServer}/add_batch.php',
|
|
payload: {'driver_id': driverId, 'batch_data': jsonEncode(batch)},
|
|
);
|
|
} catch (e) {
|
|
print('❌ Failed to upload batch: $e');
|
|
}
|
|
}
|
|
|
|
void _listenToBatteryChanges() async {
|
|
_battery.onBatteryStateChanged.listen((state) async {
|
|
int level = await _battery.batteryLevel;
|
|
bool previousMode = _isPowerSavingMode;
|
|
if (level <= powerSaveTriggerLevel) _isPowerSavingMode = true;
|
|
if (level >= powerSaveExitLevel) _isPowerSavingMode = false;
|
|
if (previousMode != _isPowerSavingMode) {
|
|
_startBatchTimers();
|
|
startLocationUpdates();
|
|
}
|
|
});
|
|
}
|
|
|
|
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) {
|
|
print('SQLite Error: $e');
|
|
}
|
|
}
|
|
|
|
// استبدال دالة Haversine اليدوية بـ Geolocator في باقي الكود أيضاً
|
|
// لأنها تعتمد على C++ في الأندرويد و Obj-C في الآيفون (Native Speed)
|
|
double _calculateDistance(LatLng a, LatLng b) {
|
|
return geo.Geolocator.distanceBetween(
|
|
a.latitude, a.longitude, b.latitude, b.longitude);
|
|
}
|
|
|
|
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;
|
|
_lastSpeedAt = now;
|
|
return null;
|
|
}
|
|
|
|
Future<void> _initLocationSettings() async {
|
|
if (await _ensureServiceAndPermission()) {
|
|
try {
|
|
await location.enableBackgroundMode(enable: true);
|
|
location.changeSettings(
|
|
accuracy: LocationAccuracy.navigation,
|
|
interval: 1000,
|
|
distanceFilter: 10);
|
|
} catch (e) {
|
|
print("Warning: $e");
|
|
}
|
|
}
|
|
}
|
|
|
|
// 🔥🔥 هذه هي الدالة المعدلة التي تستخدم ph.Permission 🔥🔥
|
|
Future<bool> _ensureServiceAndPermission() async {
|
|
// 1. طلب إذن الإشعارات أولاً باستخدام permission_handler
|
|
if (Platform.isAndroid) {
|
|
var notificationStatus = await ph.Permission.notification.status;
|
|
if (!notificationStatus.isGranted) {
|
|
await ph.Permission.notification.request();
|
|
}
|
|
}
|
|
|
|
// 2. طلب تفعيل خدمة الموقع (GPS) من بكج location
|
|
bool serviceEnabled = await location.serviceEnabled();
|
|
if (!serviceEnabled) {
|
|
serviceEnabled = await location.requestService();
|
|
if (!serviceEnabled) return false;
|
|
}
|
|
|
|
// 3. طلب إذن الموقع الأساسي من بكج location
|
|
PermissionStatus permissionGranted = await location.hasPermission();
|
|
if (permissionGranted == PermissionStatus.denied) {
|
|
permissionGranted = await location.requestPermission();
|
|
if (permissionGranted != PermissionStatus.granted) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
// ... (باقي الكود)
|
|
|
|
Future<LocationData?> getLocation() async {
|
|
try {
|
|
if (await _ensureServiceAndPermission()) {
|
|
return await location.getLocation();
|
|
}
|
|
} catch (e) {
|
|
print('❌ FAILED to get single location: $e');
|
|
}
|
|
return null;
|
|
}
|
|
}
|