import 'package:geolocator/geolocator.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../core/api_client.dart'; class LocationService { static const _channel = MethodChannel('smartplus/delivery_tracking'); static Map resolveTrackingConnection({ String? storedBaseUrl, String? storedTenantHost, }) { return { 'baseUrl': ApiClient.normalizeBaseUrl( storedBaseUrl == null || storedBaseUrl.isEmpty ? ApiClient.baseUrl : storedBaseUrl, ), 'tenantHost': storedTenantHost == null || storedTenantHost.isEmpty ? ApiClient.tenantHost : storedTenantHost, }; } Future> _trackingConnection() async { final prefs = await SharedPreferences.getInstance(); return resolveTrackingConnection( storedBaseUrl: prefs.getString('delivery_api_base_url'), storedTenantHost: prefs.getString('delivery_tenant_host'), ); } Future startNativeTracking(String routeId) async { if (defaultTargetPlatform != TargetPlatform.android) return false; try { final connection = await _trackingConnection(); return await _channel.invokeMethod('start', { 'routeId': routeId, ...connection, }) ?? false; } on PlatformException { return false; } } Future startNativeWorkdayTracking(String sessionId) async { if (defaultTargetPlatform != TargetPlatform.android) return false; try { final connection = await _trackingConnection(); return await _channel.invokeMethod('start', { 'trackingMode': 'workday', 'sessionId': sessionId, ...connection, }) ?? false; } on PlatformException { return false; } } Future stopNativeTracking() async { if (defaultTargetPlatform != TargetPlatform.android) return; try { await _channel.invokeMethod('stop'); } on PlatformException catch (_) {} } /// Checks the timestamp written by the Android foreground service. A short /// grace period prevents a false warning while the first GPS fix is being /// acquired after starting a route. Future nativeTrackingHealthy(String routeId) async { if (defaultTargetPlatform != TargetPlatform.android) return true; final prefs = await SharedPreferences.getInstance(); await prefs.reload(); final now = DateTime.now().millisecondsSinceEpoch; final started = prefs.getInt('delivery_native_tracking_started_$routeId'); final lastLocation = prefs.getInt('delivery_native_last_location_$routeId'); if (started != null && now - started <= const Duration(seconds: 90).inMilliseconds) { return true; } return lastLocation != null && now - lastLocation <= const Duration(minutes: 2).inMilliseconds; } Future requestPermission() async { if (!await Geolocator.isLocationServiceEnabled()) { await Geolocator.openLocationSettings(); return false; } var permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.deniedForever) { await Geolocator.openAppSettings(); return false; } return permission == LocationPermission.whileInUse || permission == LocationPermission.always; } Future hasUsablePermission() async { if (!await Geolocator.isLocationServiceEnabled()) return false; final permission = await Geolocator.checkPermission(); return permission == LocationPermission.whileInUse || permission == LocationPermission.always; } Stream activeRouteStream() { if (defaultTargetPlatform == TargetPlatform.android) { return Geolocator.getPositionStream( locationSettings: AndroidSettings( accuracy: LocationAccuracy.medium, distanceFilter: 50, intervalDuration: const Duration(seconds: 30), foregroundNotificationConfig: const ForegroundNotificationConfig( notificationTitle: 'SmartPlus: reparto activo', notificationText: 'Tu ubicación se comparte durante la ruta.', notificationChannelName: 'Ruta de reparto', setOngoing: true, ), ), ); } return Geolocator.getPositionStream( locationSettings: const LocationSettings( accuracy: LocationAccuracy.medium, distanceFilter: 50, ), ); } Future currentPosition() async { try { if (!await Geolocator.isLocationServiceEnabled()) return null; final permission = await Geolocator.checkPermission(); if (permission != LocationPermission.whileInUse && permission != LocationPermission.always) { return null; } return await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.medium, ), ).timeout(const Duration(seconds: 8)); } catch (_) { // A recent fix is preferable to confirming without coordinates when // the GPS provider is temporarily slow or the device is indoors. try { return await Geolocator.getLastKnownPosition(); } catch (_) { return null; } } } }