Trying again to fix GPS.

This commit is contained in:
stopflock
2025-12-22 16:35:02 -06:00
parent 9db7c11a49
commit 89fb0d9bbd
+139 -143
View File
@@ -10,8 +10,11 @@ import '../../services/proximity_alert_service.dart';
import '../../models/osm_node.dart'; import '../../models/osm_node.dart';
import '../../models/node_profile.dart'; import '../../models/node_profile.dart';
/// Manages GPS location tracking, follow-me modes, and location-based map animations. /// Simple GPS controller that respects permissions and provides location updates.
/// Handles GPS permissions, position streams, and follow-me behavior. /// Key principles:
/// - Respect "denied forever" - stop trying
/// - Retry "denied" - user might enable later
/// - Accept whatever accuracy is available once granted
class GpsController { class GpsController {
StreamSubscription<Position>? _positionSub; StreamSubscription<Position>? _positionSub;
Timer? _retryTimer; Timer? _retryTimer;
@@ -19,12 +22,8 @@ class GpsController {
// Location state // Location state
LatLng? _currentLocation; LatLng? _currentLocation;
bool _hasLocation = false; bool _hasLocation = false;
bool _hasApproximateOnly = false; // Track if we only have approximate location access
// Current tracking settings // Callbacks - set during initialization
FollowMeMode _currentFollowMeMode = FollowMeMode.off;
// Callbacks - set once during initialization
AnimatedMapController? _mapController; AnimatedMapController? _mapController;
VoidCallback? _onLocationUpdated; VoidCallback? _onLocationUpdated;
FollowMeMode Function()? _getCurrentFollowMeMode; FollowMeMode Function()? _getCurrentFollowMeMode;
@@ -40,7 +39,7 @@ class GpsController {
/// Whether we currently have a valid GPS location /// Whether we currently have a valid GPS location
bool get hasLocation => _hasLocation; bool get hasLocation => _hasLocation;
/// Initialize GPS tracking with callbacks for UI integration /// Initialize GPS tracking with callbacks
Future<void> initialize({ Future<void> initialize({
required AnimatedMapController mapController, required AnimatedMapController mapController,
required VoidCallback onLocationUpdated, required VoidCallback onLocationUpdated,
@@ -73,121 +72,110 @@ class GpsController {
required FollowMeMode oldMode, required FollowMeMode oldMode,
}) { }) {
debugPrint('[GpsController] Follow-me mode changed: $oldMode$newMode'); debugPrint('[GpsController] Follow-me mode changed: $oldMode$newMode');
_currentFollowMeMode = newMode;
// Restart tracking with new frequency // Restart position stream with new frequency settings
_startLocationTracking(); _restartPositionStream();
// Handle initial animation when follow-me is first enabled // Handle initial animation when follow-me is first enabled
if (newMode != FollowMeMode.off && _handleInitialFollowMeAnimation(newMode, oldMode);
oldMode == FollowMeMode.off &&
_currentLocation != null &&
_mapController != null) {
_animateToCurrentLocation(newMode);
}
} }
/// Manually retry location initialization (e.g., after permission granted) /// Manual retry (e.g., user pressed follow-me button)
Future<void> retryLocationInit() async { Future<void> retryLocationInit() async {
debugPrint('[GpsController] Manual retry of location initialization'); debugPrint('[GpsController] Manual retry of location initialization');
_cancelRetry(); _cancelRetry();
await _startLocationTracking(); await _startLocationTracking();
} }
/// Start or restart GPS location tracking /// Start location tracking - checks permissions and starts stream
Future<void> _startLocationTracking() async { Future<void> _startLocationTracking() async {
_stopLocationTracking(); // Clean slate _stopLocationTracking(); // Clean slate
// Check location services availability
if (!await _checkLocationAvailability()) {
_scheduleRetry();
return;
}
// Determine frequency settings based on current follow-me mode
final settings = _getLocationSettings();
final accuracyType = _hasApproximateOnly ? 'approximate' : 'precise';
final frequencyType = _currentFollowMeMode == FollowMeMode.off ? 'standard' : 'high';
debugPrint('[GpsController] Starting GPS position stream ($frequencyType frequency, $accuracyType accuracy)');
try {
_positionSub = Geolocator.getPositionStream(locationSettings: settings).listen(
_onPositionReceived,
onError: _onPositionError,
);
} catch (e) {
debugPrint('[GpsController] Failed to start position stream: $e');
_hasLocation = false;
_scheduleRetry();
}
}
/// Check if location services are available and permissions are granted
Future<bool> _checkLocationAvailability() async {
// Reset approximate-only flag
_hasApproximateOnly = false;
// Check if location services are enabled // Check if location services are enabled
bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) { if (!serviceEnabled) {
debugPrint('[GpsController] Location services disabled'); debugPrint('[GpsController] Location services disabled');
_hasLocation = false; _hasLocation = false;
return false; _notifyLocationChange();
_scheduleRetry();
return;
} }
// Check permissions // Check permissions
final perm = await Geolocator.requestPermission(); final permission = await Geolocator.requestPermission();
debugPrint('[GpsController] Location permission result: $perm'); debugPrint('[GpsController] Location permission result: $permission');
if (perm == LocationPermission.whileInUse || perm == LocationPermission.always) { switch (permission) {
debugPrint('[GpsController] Precise location permission granted: $perm'); case LocationPermission.deniedForever:
return true; // User said "never" - respect that and stop trying
} debugPrint('[GpsController] Location denied forever - stopping attempts');
_hasLocation = false;
if (perm == LocationPermission.denied || perm == LocationPermission.deniedForever) { _notifyLocationChange();
// Try approximate location as fallback return;
debugPrint('[GpsController] Precise location permission denied, trying approximate location');
try { case LocationPermission.denied:
await Geolocator.getCurrentPosition( // User said "not now" - keep trying later
desiredAccuracy: LocationAccuracy.low, debugPrint('[GpsController] Location denied - will retry later');
timeLimit: const Duration(seconds: 10), _hasLocation = false;
); _notifyLocationChange();
debugPrint('[GpsController] Approximate location available'); _scheduleRetry();
_hasApproximateOnly = true; return;
return true;
} catch (e) { case LocationPermission.whileInUse:
debugPrint('[GpsController] Approximate location also unavailable: $e'); case LocationPermission.always:
} // Permission granted - start stream
} debugPrint('[GpsController] Location permission granted: $permission');
_startPositionStream();
debugPrint('[GpsController] Location unavailable, permission: $perm'); return;
_hasLocation = false;
return false; case LocationPermission.unableToDetermine:
} // Couldn't determine permission state - treat like denied and retry
debugPrint('[GpsController] Unable to determine permission state - will retry');
/// Get location settings based on current follow-me mode and available accuracy _hasLocation = false;
LocationSettings _getLocationSettings() { _notifyLocationChange();
// Use appropriate accuracy based on what we have access to _scheduleRetry();
final accuracy = _hasApproximateOnly ? LocationAccuracy.low : LocationAccuracy.high; return;
if (_currentFollowMeMode != FollowMeMode.off) {
// High frequency for follow-me modes
return LocationSettings(
accuracy: accuracy,
distanceFilter: 1, // Update when moved 1+ meter
);
} else {
// Standard frequency when not following
return LocationSettings(
accuracy: accuracy,
distanceFilter: 5, // Update when moved 5+ meters
);
} }
} }
/// Handle position updates from GPS stream /// Start the GPS position stream
void _startPositionStream() {
final followMeMode = _getCurrentFollowMeMode?.call() ?? FollowMeMode.off;
final distanceFilter = followMeMode == FollowMeMode.off ? 5 : 1; // 5m normal, 1m follow-me
debugPrint('[GpsController] Starting GPS position stream (${distanceFilter}m filter)');
try {
_positionSub = Geolocator.getPositionStream(
locationSettings: LocationSettings(
accuracy: LocationAccuracy.high, // Request best, accept what we get
distanceFilter: distanceFilter,
),
).listen(
_onPositionReceived,
onError: _onPositionError,
);
} catch (e) {
debugPrint('[GpsController] Failed to start position stream: $e');
_hasLocation = false;
_notifyLocationChange();
_scheduleRetry();
}
}
/// Restart position stream with current follow-me settings
void _restartPositionStream() {
if (_positionSub == null) {
// No active stream, let retry logic handle it
return;
}
debugPrint('[GpsController] Restarting position stream for follow-me mode change');
_stopLocationTracking();
_startPositionStream();
}
/// Handle incoming GPS position
void _onPositionReceived(Position position) { void _onPositionReceived(Position position) {
final newLocation = LatLng(position.latitude, position.longitude); final newLocation = LatLng(position.latitude, position.longitude);
_currentLocation = newLocation; _currentLocation = newLocation;
@@ -196,12 +184,12 @@ class GpsController {
debugPrint('[GpsController] GPS location acquired'); debugPrint('[GpsController] GPS location acquired');
} }
_hasLocation = true; _hasLocation = true;
_cancelRetry(); _cancelRetry(); // Got location, stop any retry attempts
debugPrint('[GpsController] GPS position updated: ${newLocation.latitude}, ${newLocation.longitude} (accuracy: ${position.accuracy}m)'); debugPrint('[GpsController] GPS position: ${newLocation.latitude}, ${newLocation.longitude} (±${position.accuracy}m)');
// Notify UI that location was updated // Notify UI
_onLocationUpdated?.call(); _notifyLocationChange();
// Handle proximity alerts // Handle proximity alerts
_checkProximityAlerts(newLocation); _checkProximityAlerts(newLocation);
@@ -210,51 +198,48 @@ class GpsController {
_handleFollowMeUpdate(position, newLocation); _handleFollowMeUpdate(position, newLocation);
} }
/// Handle position stream errors /// Handle GPS stream errors
void _onPositionError(error) { void _onPositionError(dynamic error) {
debugPrint('[GpsController] Position stream error: $error'); debugPrint('[GpsController] Position stream error: $error');
if (_hasLocation) { if (_hasLocation) {
debugPrint('[GpsController] GPS location lost, starting retry attempts'); debugPrint('[GpsController] Lost GPS location - will retry');
} }
_hasLocation = false; _hasLocation = false;
_currentLocation = null; _currentLocation = null;
_onLocationUpdated?.call(); _notifyLocationChange();
_scheduleRetry(); _scheduleRetry();
} }
/// Check proximity alerts if enabled /// Check proximity alerts if enabled
void _checkProximityAlerts(LatLng userLocation) { void _checkProximityAlerts(LatLng userLocation) {
final proximityEnabled = _getProximityAlertsEnabled?.call() ?? false; final proximityEnabled = _getProximityAlertsEnabled?.call() ?? false;
final nearbyNodes = _getNearbyNodes?.call() ?? []; if (!proximityEnabled) return;
if (proximityEnabled && nearbyNodes.isNotEmpty) { final nearbyNodes = _getNearbyNodes?.call() ?? [];
final alertDistance = _getProximityAlertDistance?.call() ?? 200; if (nearbyNodes.isEmpty) return;
final enabledProfiles = _getEnabledProfiles?.call() ?? [];
final alertDistance = _getProximityAlertDistance?.call() ?? 200;
ProximityAlertService().checkProximity( final enabledProfiles = _getEnabledProfiles?.call() ?? [];
userLocation: userLocation,
nodes: nearbyNodes, ProximityAlertService().checkProximity(
enabledProfiles: enabledProfiles, userLocation: userLocation,
alertDistance: alertDistance, nodes: nearbyNodes,
); enabledProfiles: enabledProfiles,
} alertDistance: alertDistance,
);
} }
/// Handle follow-me animations and map updates /// Handle follow-me animations
void _handleFollowMeUpdate(Position position, LatLng location) { void _handleFollowMeUpdate(Position position, LatLng location) {
// Get current follow-me mode from app state (in case it changed)
final followMeMode = _getCurrentFollowMeMode?.call() ?? FollowMeMode.off; final followMeMode = _getCurrentFollowMeMode?.call() ?? FollowMeMode.off;
if (followMeMode == FollowMeMode.off || _mapController == null) { if (followMeMode == FollowMeMode.off || _mapController == null) {
return; // Not following or no map controller return;
} }
debugPrint('[GpsController] GPS position update for follow-me: ${location.latitude}, ${location.longitude}, mode: $followMeMode');
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
try { try {
if (followMeMode == FollowMeMode.follow) { if (followMeMode == FollowMeMode.follow) {
// Follow position only, preserve current rotation // Follow position, preserve rotation
_mapController!.animateTo( _mapController!.animateTo(
dest: location, dest: location,
zoom: _mapController!.mapController.camera.zoom, zoom: _mapController!.mapController.camera.zoom,
@@ -263,11 +248,11 @@ class GpsController {
curve: Curves.easeOut, curve: Curves.easeOut,
); );
} else if (followMeMode == FollowMeMode.rotating) { } else if (followMeMode == FollowMeMode.rotating) {
// Follow position and rotation based on heading // Follow position and heading
final heading = position.heading; final heading = position.heading;
final speed = position.speed; final speed = position.speed;
// Only apply rotation if moving fast enough to avoid wild spinning // Only rotate if moving fast enough and heading is valid
final shouldRotate = !speed.isNaN && speed >= kMinSpeedForRotationMps && !heading.isNaN; final shouldRotate = !speed.isNaN && speed >= kMinSpeedForRotationMps && !heading.isNaN;
final rotation = shouldRotate ? -heading : _mapController!.mapController.camera.rotation; final rotation = shouldRotate ? -heading : _mapController!.mapController.camera.rotation;
@@ -280,28 +265,34 @@ class GpsController {
); );
} }
// Notify that we moved the map programmatically (for node refresh) // Notify that map was moved programmatically
_onMapMovedProgrammatically?.call(); _onMapMovedProgrammatically?.call();
} catch (e) { } catch (e) {
debugPrint('[GpsController] MapController not ready for position animation: $e'); debugPrint('[GpsController] Map animation error: $e');
} }
}); });
} }
/// Animate to current location when follow-me is first enabled /// Handle initial animation when follow-me mode is enabled
void _animateToCurrentLocation(FollowMeMode mode) { void _handleInitialFollowMeAnimation(FollowMeMode newMode, FollowMeMode oldMode) {
if (_currentLocation == null || _mapController == null) return; if (newMode == FollowMeMode.off || oldMode != FollowMeMode.off) {
return; // Not enabling follow-me, or already enabled
}
if (_currentLocation == null || _mapController == null) {
return; // No location or map controller
}
try { try {
if (mode == FollowMeMode.follow) { if (newMode == FollowMeMode.follow) {
_mapController!.animateTo( _mapController!.animateTo(
dest: _currentLocation!, dest: _currentLocation!,
zoom: _mapController!.mapController.camera.zoom, zoom: _mapController!.mapController.camera.zoom,
duration: kFollowMeAnimationDuration, duration: kFollowMeAnimationDuration,
curve: Curves.easeOut, curve: Curves.easeOut,
); );
} else if (mode == FollowMeMode.rotating) { } else if (newMode == FollowMeMode.rotating) {
// When switching to rotating mode, reset to north-up first // Reset to north-up when starting rotating mode
_mapController!.animateTo( _mapController!.animateTo(
dest: _currentLocation!, dest: _currentLocation!,
zoom: _mapController!.mapController.camera.zoom, zoom: _mapController!.mapController.camera.zoom,
@@ -313,35 +304,40 @@ class GpsController {
_onMapMovedProgrammatically?.call(); _onMapMovedProgrammatically?.call();
} catch (e) { } catch (e) {
debugPrint('[GpsController] MapController not ready for initial follow-me animation: $e'); debugPrint('[GpsController] Initial follow-me animation error: $e');
} }
} }
/// Schedule periodic retry attempts to get location /// Notify UI that location status changed
void _notifyLocationChange() {
_onLocationUpdated?.call();
}
/// Schedule retry attempts for location access
void _scheduleRetry() { void _scheduleRetry() {
_retryTimer?.cancel(); _cancelRetry();
_retryTimer = Timer.periodic(const Duration(seconds: 15), (timer) { _retryTimer = Timer.periodic(const Duration(seconds: 15), (timer) {
debugPrint('[GpsController] Automatic retry of location initialization (attempt ${timer.tick})'); debugPrint('[GpsController] Retry attempt ${timer.tick}');
_startLocationTracking(); _startLocationTracking();
}); });
} }
/// Cancel any scheduled retry attempts /// Cancel any pending retry attempts
void _cancelRetry() { void _cancelRetry() {
if (_retryTimer != null) { if (_retryTimer != null) {
debugPrint('[GpsController] Canceling location retry timer'); debugPrint('[GpsController] Canceling retry timer');
_retryTimer?.cancel(); _retryTimer?.cancel();
_retryTimer = null; _retryTimer = null;
} }
} }
/// Stop location tracking and clean up /// Stop the position stream
void _stopLocationTracking() { void _stopLocationTracking() {
_positionSub?.cancel(); _positionSub?.cancel();
_positionSub = null; _positionSub = null;
} }
/// Dispose of all GPS resources /// Clean up all resources
void dispose() { void dispose() {
debugPrint('[GpsController] Disposing GPS controller'); debugPrint('[GpsController] Disposing GPS controller');
_stopLocationTracking(); _stopLocationTracking();