mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-07-17 09:37:28 +02:00
Trying again to fix GPS.
This commit is contained in:
+133
-137
@@ -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;
|
||||||
|
_notifyLocationChange();
|
||||||
|
return;
|
||||||
|
|
||||||
if (perm == LocationPermission.denied || perm == LocationPermission.deniedForever) {
|
case LocationPermission.denied:
|
||||||
// Try approximate location as fallback
|
// User said "not now" - keep trying later
|
||||||
debugPrint('[GpsController] Precise location permission denied, trying approximate location');
|
debugPrint('[GpsController] Location denied - will retry later');
|
||||||
try {
|
_hasLocation = false;
|
||||||
await Geolocator.getCurrentPosition(
|
_notifyLocationChange();
|
||||||
desiredAccuracy: LocationAccuracy.low,
|
_scheduleRetry();
|
||||||
timeLimit: const Duration(seconds: 10),
|
return;
|
||||||
);
|
|
||||||
debugPrint('[GpsController] Approximate location available');
|
|
||||||
_hasApproximateOnly = true;
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('[GpsController] Approximate location also unavailable: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint('[GpsController] Location unavailable, permission: $perm');
|
case LocationPermission.whileInUse:
|
||||||
_hasLocation = false;
|
case LocationPermission.always:
|
||||||
return false;
|
// Permission granted - start stream
|
||||||
}
|
debugPrint('[GpsController] Location permission granted: $permission');
|
||||||
|
_startPositionStream();
|
||||||
|
return;
|
||||||
|
|
||||||
/// Get location settings based on current follow-me mode and available accuracy
|
case LocationPermission.unableToDetermine:
|
||||||
LocationSettings _getLocationSettings() {
|
// Couldn't determine permission state - treat like denied and retry
|
||||||
// Use appropriate accuracy based on what we have access to
|
debugPrint('[GpsController] Unable to determine permission state - will retry');
|
||||||
final accuracy = _hasApproximateOnly ? LocationAccuracy.low : LocationAccuracy.high;
|
_hasLocation = false;
|
||||||
|
_notifyLocationChange();
|
||||||
if (_currentFollowMeMode != FollowMeMode.off) {
|
_scheduleRetry();
|
||||||
// High frequency for follow-me modes
|
return;
|
||||||
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;
|
||||||
|
if (!proximityEnabled) return;
|
||||||
|
|
||||||
final nearbyNodes = _getNearbyNodes?.call() ?? [];
|
final nearbyNodes = _getNearbyNodes?.call() ?? [];
|
||||||
|
if (nearbyNodes.isEmpty) return;
|
||||||
|
|
||||||
if (proximityEnabled && nearbyNodes.isNotEmpty) {
|
final alertDistance = _getProximityAlertDistance?.call() ?? 200;
|
||||||
final alertDistance = _getProximityAlertDistance?.call() ?? 200;
|
final enabledProfiles = _getEnabledProfiles?.call() ?? [];
|
||||||
final enabledProfiles = _getEnabledProfiles?.call() ?? [];
|
|
||||||
|
|
||||||
ProximityAlertService().checkProximity(
|
ProximityAlertService().checkProximity(
|
||||||
userLocation: userLocation,
|
userLocation: userLocation,
|
||||||
nodes: nearbyNodes,
|
nodes: nearbyNodes,
|
||||||
enabledProfiles: enabledProfiles,
|
enabledProfiles: enabledProfiles,
|
||||||
alertDistance: alertDistance,
|
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();
|
||||||
|
|||||||
Reference in New Issue
Block a user