Enhance validation and error handling across various screens

- Updated ConnectionProvider to track pending messages before sending to avoid race conditions.
- Integrated ValidationService in DeviceConfigScreen for validating latitude, longitude, frequency, and TX power inputs.
- Added bounds and zoom level validation in MapManagementScreen using ValidationService.
- Refactored MapTab to utilize LocationTrackingService for location updates and removed deprecated location permission handling.
- Replaced BackgroundLocationService with LocationTrackingService in SettingsScreen, improving location update management and error handling.
- Enhanced SAR Update Sheet to validate coordinates, notes length, and location accuracy before sending SAR markers.
This commit is contained in:
Janez T
2025-10-15 11:09:34 +02:00
parent 823a3c50f7
commit a6e317bc37
8 changed files with 822 additions and 378 deletions

View File

@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:geolocator/geolocator.dart';
import '../providers/connection_provider.dart';
import '../services/validation_service.dart';
class DeviceConfigScreen extends StatefulWidget {
const DeviceConfigScreen({super.key});
@@ -106,6 +107,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
Future<void> _savePublicInfo() async {
final connectionProvider = context.read<ConnectionProvider>();
final deviceInfo = connectionProvider.deviceInfo;
final validator = ValidationService();
try {
// Save name
@@ -115,11 +117,36 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
// Save position and telemetry settings
if (_telemetryEnabled) {
final lat = double.tryParse(_latController.text) ?? 0.0;
final lon = double.tryParse(_lonController.text) ?? 0.0;
// Parse and validate coordinates
final latResult = validator.parseLatitude(_latController.text);
if (!latResult.isSuccess) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(latResult.errorMessage!),
backgroundColor: Colors.red,
),
);
}
return;
}
final lonResult = validator.parseLongitude(_lonController.text);
if (!lonResult.isSuccess) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(lonResult.errorMessage!),
backgroundColor: Colors.red,
),
);
}
return;
}
await connectionProvider.setAdvertLatLon(
latitude: lat,
longitude: lon,
latitude: latResult.value!,
longitude: lonResult.value!,
);
// Set telemetry modes to "Allow All" (mode 2 for both base and location)
@@ -167,21 +194,53 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
Future<void> _saveRadioSettings() async {
final connectionProvider = context.read<ConnectionProvider>();
final validator = ValidationService();
final deviceInfo = connectionProvider.deviceInfo;
try {
// Parse and save frequency (convert from MHz to kHz)
final freq = (double.tryParse(_freqController.text) ?? 869.618) * 1000;
// Parse and validate frequency
final freqResult = validator.parseFrequency(_freqController.text);
if (!freqResult.isSuccess) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(freqResult.errorMessage!),
backgroundColor: Colors.red,
),
);
}
return;
}
// Parse and validate TX power
final txPowerResult = validator.parseTxPower(
_txPowerController.text,
maxPower: deviceInfo.maxTxPower,
);
if (!txPowerResult.isSuccess) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(txPowerResult.errorMessage!),
backgroundColor: Colors.red,
),
);
}
return;
}
// Convert from MHz to kHz for protocol
final freqKhz = (freqResult.value! * 1000).round();
await connectionProvider.setRadioParams(
frequency: freq.round(),
frequency: freqKhz,
bandwidth: _bandwidthToValue(_selectedBandwidth),
spreadingFactor: _selectedSpreadingFactor,
codingRate: _selectedCodingRate,
);
// Save TX power
final txPower = int.tryParse(_txPowerController.text) ?? 20;
await connectionProvider.setTxPower(txPower);
await connectionProvider.setTxPower(txPowerResult.value!);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(

View File

@@ -4,6 +4,7 @@ import 'package:latlong2/latlong.dart';
import 'package:file_picker/file_picker.dart';
import 'package:share_plus/share_plus.dart';
import '../services/tile_cache_service.dart';
import '../services/validation_service.dart';
import '../models/map_layer.dart';
class MapManagementScreen extends StatefulWidget {
@@ -108,25 +109,49 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
}
Future<void> _downloadRegion() async {
final validator = ValidationService();
try {
// Parse coordinates
final north = double.tryParse(_northController.text);
final south = double.tryParse(_southController.text);
final east = double.tryParse(_eastController.text);
final west = double.tryParse(_westController.text);
if (north == null || south == null || east == null || west == null) {
_showError('Invalid coordinates. Please enter valid numbers.');
// Validate bounds
final boundsResult = validator.validateBounds(
north: north,
south: south,
east: east,
west: west,
);
if (!boundsResult.isValid) {
_showError(boundsResult.errorMessage!);
return;
}
if (north <= south || east <= west) {
_showError('Invalid bounds. North must be > South, East must be > West.');
// Validate zoom levels
final minZoomResult = validator.validateZoomLevel(_minZoom);
if (!minZoomResult.isValid) {
_showError('Min zoom: ${minZoomResult.errorMessage}');
return;
}
final maxZoomResult = validator.validateZoomLevel(_maxZoom);
if (!maxZoomResult.isValid) {
_showError('Max zoom: ${maxZoomResult.errorMessage}');
return;
}
if (_minZoom > _maxZoom) {
_showError('Minimum zoom must be less than or equal to maximum zoom');
return;
}
final bounds = LatLngBounds(
LatLng(south, west),
LatLng(north, east),
LatLng(south!, west!),
LatLng(north!, east!),
);
if (!mounted) return;

View File

@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
@@ -19,7 +18,8 @@ import '../models/map_layer.dart';
import '../models/message.dart';
import '../services/tile_cache_service.dart';
import '../services/background_location_service.dart';
import '../widgets/map_markers.dart';
import '../services/location_tracking_service.dart';
import '../services/map_marker_service.dart';
import '../widgets/map_debug_info.dart';
import '../widgets/map/map_legend.dart';
import '../widgets/map/compass_widget.dart';
@@ -37,17 +37,17 @@ class MapTab extends StatefulWidget {
class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
final MapController _mapController = MapController();
final TileCacheService _tileCache = TileCacheService();
final LocationTrackingService _locationService = LocationTrackingService();
final MapMarkerService _markerService = MapMarkerService();
bool _isInitialized = false;
bool _isMapReady = false; // Track when map widget is actually rendered
MapLayer _currentLayer = MapLayer.openStreetMap;
Position? _currentPosition;
double? _compassHeading; // Compass sensor heading
bool _rotateMarkerWithHeading = false; // Toggle for rotation
bool _showLegend = false;
bool _showMapDebugInfo = false; // Toggle for debug info
double _gpsUpdateDistance = 3.0; // meters
bool _backgroundTrackingEnabled = false; // Toggle for background tracking
StreamSubscription<Position>? _positionStreamSubscription;
StreamSubscription<CompassEvent>? _compassStreamSubscription;
final BackgroundLocationService _backgroundLocationService = BackgroundLocationService();
@@ -72,7 +72,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
super.initState();
_loadSettings();
_initializeTileCache();
_requestLocationPermission();
_initLocationTracking();
_startCompassTracking();
// Listen to map provider for navigation requests
@@ -89,6 +89,45 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
});
}
Future<void> _initLocationTracking() async {
// Initialize LocationTrackingService
final appProvider = context.read<AppProvider>();
await _locationService.initialize(appProvider.connectionProvider.bleService);
// Set up callbacks
_locationService.onPositionUpdate = (position) {
if (mounted) {
setState(() {
// Position updates are now handled by the service
});
// Rotate map if rotation mode is enabled and heading is available
if (_isMapReady && _rotateMarkerWithHeading && position.heading >= 0) {
try {
final camera = _mapController.camera;
_mapController.moveAndRotate(
camera.center,
camera.zoom,
-position.heading,
);
} catch (e) {
// Map not ready yet, ignore
}
}
}
};
_locationService.onError = (error) {
debugPrint('Location tracking error: $error');
};
// Request permissions and start tracking
final hasPermission = await _locationService.requestPermissions();
if (hasPermission) {
await _locationService.startTracking(distanceThreshold: _gpsUpdateDistance);
}
}
void _startCompassTracking() {
final compassStream = FlutterCompass.events;
if (compassStream == null) {
@@ -178,70 +217,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
}
}
Future<void> _requestLocationPermission() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return;
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return;
}
}
if (permission == LocationPermission.deniedForever) {
return;
}
// Get initial position
try {
final position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.best,
distanceFilter: 0,
),
);
if (mounted) {
setState(() {
_currentPosition = position;
});
}
} catch (e) {
debugPrint('Error getting location: $e');
}
// Start listening to location updates
_positionStreamSubscription = Geolocator.getPositionStream(
locationSettings: LocationSettings(
accuracy: LocationAccuracy.best,
distanceFilter: _gpsUpdateDistance.toInt(),
),
).listen((Position position) {
if (mounted) {
setState(() {
_currentPosition = position;
});
// Rotate map if rotation mode is enabled and heading is available
// Heading of -1.0 means heading is unavailable
if (_isMapReady && _rotateMarkerWithHeading && position.heading != null && position.heading >= 0) {
try {
final camera = _mapController.camera;
_mapController.moveAndRotate(
camera.center,
camera.zoom,
-position.heading,
);
} catch (e) {
// Map not ready yet, ignore
}
}
}
});
}
void _handleMapNavigation() {
final mapProvider = context.read<MapProvider>();
@@ -312,8 +287,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
final mapProvider = context.read<MapProvider>();
mapProvider.removeListener(_handleMapNavigation);
_positionStreamSubscription?.cancel();
_compassStreamSubscription?.cancel();
_locationService.stopTracking();
_mapController.dispose();
_tileCache.dispose();
super.dispose();
@@ -326,8 +301,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
return _compassHeading;
}
// Fall back to GPS heading when moving
if (_currentPosition?.heading != null && _currentPosition!.heading >= 0) {
return _currentPosition!.heading;
final currentPosition = _locationService.currentPosition;
if (currentPosition?.heading != null && currentPosition!.heading >= 0) {
return currentPosition.heading;
}
return null;
}
@@ -344,27 +320,11 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
}
LatLng _calculateCenter(List<Contact> contacts, List<SarMarker> sarMarkers) {
final allPoints = <LatLng>[];
for (final contact in contacts) {
if (contact.displayLocation != null) {
allPoints.add(contact.displayLocation!);
}
}
for (final marker in sarMarkers) {
allPoints.add(marker.location);
}
if (allPoints.isEmpty) return _defaultCenter;
double lat = 0, lng = 0;
for (final point in allPoints) {
lat += point.latitude;
lng += point.longitude;
}
return LatLng(lat / allPoints.length, lng / allPoints.length);
return _markerService.calculateCenter(
contacts: contacts,
sarMarkers: sarMarkers,
defaultCenter: _defaultCenter,
);
}
void _showLayerSelector(BuildContext context) {
@@ -556,7 +516,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: DetailedCompassDialog(
initialPosition: _currentPosition,
initialPosition: _locationService.currentPosition,
initialHeading: _currentHeading,
contacts: contacts,
sarMarkers: sarMarkers,
@@ -582,7 +542,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: DetailedCompassDialog(
initialPosition: _currentPosition,
initialPosition: _locationService.currentPosition,
initialHeading: _currentHeading,
contacts: contacts,
sarMarkers: sarMarkers,
@@ -609,7 +569,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: DetailedCompassDialog(
initialPosition: _currentPosition,
initialPosition: _locationService.currentPosition,
initialHeading: _currentHeading,
contacts: contacts,
sarMarkers: sarMarkers,
@@ -620,37 +580,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
}
void _restartLocationStream() {
// Cancel existing subscription
_positionStreamSubscription?.cancel();
// Start new stream with updated distance
_positionStreamSubscription = Geolocator.getPositionStream(
locationSettings: LocationSettings(
accuracy: LocationAccuracy.best,
distanceFilter: _gpsUpdateDistance.toInt(),
),
).listen((Position position) {
if (mounted) {
setState(() {
_currentPosition = position;
});
// Rotate map if rotation mode is enabled and heading is available
// Heading of -1.0 means heading is unavailable
if (_isMapReady && _rotateMarkerWithHeading && position.heading != null && position.heading >= 0) {
try {
final camera = _mapController.camera;
_mapController.moveAndRotate(
camera.center,
camera.zoom,
-position.heading,
);
} catch (e) {
// Map not ready yet, ignore
}
}
}
});
// Update distance threshold in location service
_locationService.updateDistanceThreshold(_gpsUpdateDistance);
// Update background tracking distance if active
if (_backgroundTrackingEnabled) {
@@ -695,18 +626,12 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
/// Calculate distance between two points in meters
double _calculateDistanceInMeters(double lat1, double lon1, double lat2, double lon2) {
const R = 6371000; // Earth's radius in meters
final dLat = (lat2 - lat1) * pi / 180;
final dLon = (lon2 - lon1) * pi / 180;
final a = sin(dLat / 2) * sin(dLat / 2) +
cos(lat1 * pi / 180) *
cos(lat2 * pi / 180) *
sin(dLon / 2) *
sin(dLon / 2);
final c = 2 * atan2(sqrt(a), sqrt(1 - a));
return R * c;
return _markerService.calculateDistance(
lat1: lat1,
lon1: lon1,
lat2: lat2,
lon2: lon2,
);
}
/// Show SAR dialog with pre-populated location from map long press
@@ -964,11 +889,13 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
),
MarkerLayer(
markers: [
...MapMarkers.createTeamMemberMarkers(
contactsWithLocation,
context,
// Contact markers
..._markerService.generateContactMarkers(
contacts: contactsWithLocation,
context: context,
mapRotation: _getMapRotation(),
onContactTap: (contact) {
userPosition: _locationService.currentPosition,
onTap: (contact) {
_showDetailedCompassWithContact(
context,
contactsProvider.contactsWithLocation,
@@ -977,11 +904,12 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
);
},
),
...MapMarkers.createSarMarkers(
sarMarkers,
context,
// SAR markers
..._markerService.generateSarMarkers(
sarMarkers: sarMarkers,
context: context,
mapRotation: _getMapRotation(),
onSarMarkerTap: (marker) {
onTap: (marker) {
_showDetailedCompassWithSarMarker(
context,
contactsProvider.contactsWithLocation,
@@ -991,40 +919,14 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
},
),
// User location marker
if (_currentPosition != null)
Marker(
point: LatLng(
_currentPosition!.latitude,
_currentPosition!.longitude,
),
width: 40,
height: 40,
rotate: false, // Don't rotate with map
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3),
shape: BoxShape.circle,
),
child: Container(
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 4,
),
],
),
child: const Icon(
Icons.my_location,
color: Colors.white,
size: 16,
),
),
),
),
if (_markerService.generateUserLocationMarker(
position: _locationService.currentPosition,
context: context,
) != null)
_markerService.generateUserLocationMarker(
position: _locationService.currentPosition,
context: context,
)!,
// Dropped pin marker with label
if (_droppedPinLocation != null)
Marker(
@@ -1152,30 +1054,23 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
heroTag: 'center_map',
onPressed: !_isMapReady ? null : () async {
// Force update GPS location and jump to it
try {
final position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.best,
distanceFilter: 0,
),
final position = await _locationService.getCurrentPosition();
if (position != null && mounted) {
setState(() {
// Position updated in service
});
_mapController.move(
LatLng(position.latitude, position.longitude),
16,
);
if (mounted) {
setState(() {
_currentPosition = position;
});
_mapController.move(
LatLng(position.latitude, position.longitude),
16,
);
}
} catch (e) {
debugPrint('Error getting location: $e');
} else {
// Fallback to cached position or default center
if (_currentPosition != null) {
final currentPosition = _locationService.currentPosition;
if (currentPosition != null) {
_mapController.move(
LatLng(
_currentPosition!.latitude,
_currentPosition!.longitude,
currentPosition.latitude,
currentPosition.longitude,
),
16,
);

View File

@@ -7,7 +7,7 @@ import 'package:latlong2/latlong.dart';
import '../providers/contacts_provider.dart';
import '../providers/messages_provider.dart';
import '../providers/app_provider.dart';
import '../services/background_location_service.dart';
import '../services/location_tracking_service.dart';
import '../utils/sample_data_generator.dart';
import '../theme/app_theme.dart';
@@ -29,21 +29,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
late AppThemeMode _selectedTheme;
PackageInfo? _packageInfo;
bool _isLoadingSampleData = false;
double _gpsUpdateDistance = 10.0;
double _gpsMinDistance = 5.0;
double _gpsMaxDistance = 100.0;
int _minTimeIntervalSeconds = 30;
bool _backgroundTrackingEnabled = false;
bool _isSendingLocationUpdate = false;
final BackgroundLocationService _backgroundLocationService =
BackgroundLocationService();
final LocationTrackingService _locationService = LocationTrackingService();
@override
void initState() {
super.initState();
_selectedTheme = widget.currentTheme;
_loadPackageInfo();
_loadLocationSettings();
_initializeLocationService();
}
Future<void> _loadPackageInfo() async {
@@ -55,45 +49,60 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
}
Future<void> _loadLocationSettings() async {
final prefs = await SharedPreferences.getInstance();
if (mounted) {
setState(() {
_gpsUpdateDistance = prefs.getDouble('map_gps_update_distance') ?? 10.0;
_gpsMinDistance = prefs.getDouble('map_gps_min_distance') ?? 5.0;
_gpsMaxDistance = prefs.getDouble('map_gps_max_distance') ?? 100.0;
_minTimeIntervalSeconds = prefs.getInt('map_gps_min_time_interval') ?? 30;
_backgroundTrackingEnabled =
prefs.getBool('background_tracking_enabled') ?? false;
});
Future<void> _initializeLocationService() async {
// Initialize location service with BLE service
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (mounted) {
final appProvider = context.read<AppProvider>();
await _locationService.initialize(
appProvider.connectionProvider.bleService,
);
// Initialize background location service
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
final appProvider = context.read<AppProvider>();
_backgroundLocationService.initialize(
appProvider.connectionProvider.bleService,
);
// Restore background tracking state
if (_backgroundTrackingEnabled) {
_startBackgroundTracking();
// Set up callbacks for UI feedback
_locationService.onError = (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(error),
backgroundColor: Colors.orange,
),
);
}
}
});
}
}
};
Future<void> _saveLocationSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble('map_gps_update_distance', _gpsUpdateDistance);
await prefs.setDouble('map_gps_min_distance', _gpsMinDistance);
await prefs.setDouble('map_gps_max_distance', _gpsMaxDistance);
await prefs.setInt('map_gps_min_time_interval', _minTimeIntervalSeconds);
await prefs.setBool(
'background_tracking_enabled',
_backgroundTrackingEnabled,
);
_locationService.onBroadcastSent = (position) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Location broadcast: ${position.latitude.toStringAsFixed(5)}, ${position.longitude.toStringAsFixed(5)}',
),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
);
}
};
_locationService.onTrackingStateChanged = (isTracking) {
if (mounted) {
setState(() {});
}
};
// Load settings and restore tracking state
final prefs = await SharedPreferences.getInstance();
final wasTracking = prefs.getBool('background_tracking_enabled') ?? false;
if (wasTracking) {
await _startBackgroundTracking();
}
if (mounted) {
setState(() {});
}
}
});
}
Future<void> _saveThemePreference(AppThemeMode theme) async {
@@ -198,89 +207,47 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
Future<void> _startBackgroundTracking() async {
final success = await _backgroundLocationService.startTracking(
distanceThreshold: _gpsUpdateDistance,
final success = await _locationService.startTracking(
distanceThreshold: _locationService.gpsUpdateDistance,
);
if (!success) {
if (mounted) {
setState(() {
_backgroundTrackingEnabled = false;
});
_saveLocationSettings();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Failed to start background tracking. Check permissions and BLE connection.',
),
duration: Duration(seconds: 3),
if (!success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Failed to start background tracking. Check permissions and BLE connection.',
),
);
}
duration: Duration(seconds: 3),
),
);
}
if (mounted) {
setState(() {});
}
}
Future<void> _stopBackgroundTracking() async {
await _backgroundLocationService.stopTracking();
await _locationService.stopTracking();
if (mounted) {
setState(() {});
}
}
Future<void> _sendLocationUpdateNow() async {
setState(() => _isSendingLocationUpdate = true);
try {
// Get current location
Position position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.best,
),
);
final success = await _locationService.broadcastLocationNow();
if (!mounted) return;
// Get connection provider
final appProvider = context.read<AppProvider>();
final connectionProvider = appProvider.connectionProvider;
if (!connectionProvider.deviceInfo.isConnected) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Not connected to device'),
backgroundColor: Colors.orange,
),
);
}
return;
}
// Update device location
await connectionProvider.setAdvertLatLon(
latitude: position.latitude,
longitude: position.longitude,
);
// Send advertisement
await connectionProvider.sendSelfAdvert(floodMode: true);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Location broadcast: ${position.latitude.toStringAsFixed(5)}, ${position.longitude.toStringAsFixed(5)}',
if (!success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Failed to send location update'),
backgroundColor: Colors.red,
),
backgroundColor: Colors.green,
),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to send location: $e'),
backgroundColor: Colors.red,
),
);
);
}
} finally {
if (mounted) {
setState(() => _isSendingLocationUpdate = false);
@@ -383,21 +350,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
secondary: const Icon(Icons.location_on),
title: const Text('Auto Location Tracking'),
subtitle: const Text('Automatically broadcast position updates'),
value: _backgroundTrackingEnabled,
value: _locationService.isTracking,
onChanged: (value) {
setState(() {
_backgroundTrackingEnabled = value;
if (value) {
_startBackgroundTracking();
} else {
_stopBackgroundTracking();
}
});
_saveLocationSettings();
if (value) {
_startBackgroundTracking();
} else {
_stopBackgroundTracking();
}
},
),
if (_backgroundTrackingEnabled) ...[
if (_locationService.isTracking) ...[
ListTile(
leading: const Icon(Icons.tune),
title: const Text('Configure Tracking'),
@@ -509,9 +472,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
void _showTrackingConfigDialog() {
double tempMinDistance = _gpsMinDistance;
double tempMaxDistance = _gpsMaxDistance;
int tempTimeInterval = _minTimeIntervalSeconds;
double tempMinDistance = _locationService.minDistanceMeters;
double tempMaxDistance = _locationService.maxDistanceMeters;
int tempTimeInterval = _locationService.minTimeIntervalSeconds;
showDialog(
context: context,
@@ -670,23 +633,27 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
setState(() {
_gpsMinDistance = tempMinDistance;
_gpsMaxDistance = tempMaxDistance;
_minTimeIntervalSeconds = tempTimeInterval;
_gpsUpdateDistance = tempMinDistance; // Use min as the primary threshold
});
_saveLocationSettings();
onPressed: () async {
// Update location service configuration
_locationService.minDistanceMeters = tempMinDistance;
_locationService.maxDistanceMeters = tempMaxDistance;
_locationService.minTimeIntervalSeconds = tempTimeInterval;
_locationService.gpsUpdateDistance = tempMinDistance;
// Update background tracking if active
if (_backgroundTrackingEnabled) {
_backgroundLocationService.updateDistanceThreshold(
tempMinDistance,
);
// Save settings
await _locationService.saveSettings();
// Update tracking if active
if (_locationService.isTracking) {
await _locationService.updateDistanceThreshold(tempMinDistance);
}
// Close dialog before setState
Navigator.pop(context);
if (mounted) {
setState(() {});
}
},
child: const Text('Save'),
),