From ae2f4cf85e4cc95ac2c26cec975533e989c83ada Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 17 Jul 2025 19:03:41 -0700 Subject: [PATCH] How about we comment some confusing code. --- Meshtastic/Helpers/LocationsHandler.swift | 104 +++++++++++++++++----- 1 file changed, 80 insertions(+), 24 deletions(-) diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 645959f3..ea1a05cc 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -2,14 +2,15 @@ // LocationsHandler.swift // Meshtastic // -// Created by Garth Vander Houwen on 12/4/23. +// Copyright Garth Vander Houwen 12/4/23. // import SwiftUI import CoreLocation import OSLog -// Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`. +// The @MainActor annotation ensures that all state changes and UI updates happen on the main thread, +// preventing potential race conditions and crashes related to UI updates from background threads. @MainActor class LocationsHandler: NSObject, ObservableObject, @preconcurrency CLLocationManagerDelegate { static let shared = LocationsHandler() // Create a single, shared instance of the object. @@ -34,46 +35,82 @@ import OSLog @Published var backgroundActivity: Bool = UserDefaults.standard.bool(forKey: "BGActivitySessionStarted") { didSet { + // Invalidate or create the background activity session based on the new value. backgroundActivity ? self.background = CLBackgroundActivitySession() : self.background?.invalidate() UserDefaults.standard.set(backgroundActivity, forKey: "BGActivitySessionStarted") } } // The continuation we will use to asynchronously ask the user permission to track their location. + // This is an Optional to ensure it can be nilled out after use. private var permissionContinuation: CheckedContinuation? + /// Requests "Always" location authorization from the user. + /// This method uses Swift's structured concurrency to await the user's decision. + /// - Returns: The `CLAuthorizationStatus` reflecting the user's choice. func requestLocationAlwaysPermissions() async -> CLAuthorizationStatus { return await withCheckedContinuation { continuation in + // Store the continuation. self.permissionContinuation = continuation + // Request authorization. The response will come via `locationManagerDidChangeAuthorization`. manager.requestAlwaysAuthorization() } } + /// Delegate method called when the location authorization status changes. + /// - Parameter manager: The CLLocationManager instance. func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { - // This is the line you need to add - permissionContinuation?.resume(returning: manager.authorizationStatus) + // Ensure the continuation exists before attempting to resume it. + // If it's nil, it means either no request was pending or it was already resumed. + guard let continuation = permissionContinuation else { + Logger.services.debug("📍 [App] locationManagerDidChangeAuthorization called but no permissionContinuation is active.") + return + } + // Resume the continuation with the current authorization status. + continuation.resume(returning: manager.authorizationStatus) + // CRUCIAL: Nil out the continuation immediately after resuming it. + // This prevents attempting to resume the same continuation multiple times, + // which would lead to a runtime crash. + self.permissionContinuation = nil } - override init() { super.init() self.manager.delegate = self + // Allow background location updates for continuous tracking. self.manager.allowsBackgroundLocationUpdates = true + // Set desired accuracy for location updates. + // Consider your app's needs: kCLLocationAccuracyBestForNavigation, kCLLocationAccuracyBest, etc. + // For general tracking, kCLLocationAccuracyHundredMeters might be sufficient to save battery. + self.manager.desiredAccuracy = kCLLocationAccuracyBest + // Set the distance filter to only receive updates when the device has moved a certain distance. + self.manager.distanceFilter = kCLDistanceFilterNone // Receive all updates initially } func startLocationUpdates() { let status = self.manager.authorizationStatus + // Guard against starting updates without proper authorization. guard status == .authorizedAlways || status == .authorizedWhenInUse else { + Logger.services.warning("📍 [App] Cannot start location updates: insufficient authorization status: \(status.rawValue)") return } Logger.services.info("📍 [App] Starting location updates") - Task { + // Using a Task for asynchronous operations. The @MainActor isolation of the class + // ensures that all state changes within this Task (accessing @Published properties) + // will be performed on the main actor. + Task { @MainActor in do { self.updatesStarted = true + // `liveUpdates()` provides a stream of location updates. let updates = CLLocationUpdate.liveUpdates() for try await update in updates { - if !self.updatesStarted { break } + // Check for task cancellation to allow graceful stopping. + try Task.checkCancellation() + // If `updatesStarted` is set to false (e.g., by `stopLocationUpdates`), + // break out of the loop to stop processing updates. + if !self.updatesStarted { + Logger.services.info("🛑 [App] Location updates loop stopped due to updatesStarted being false.") + break + } if let loc = update.location { self.isStationary = update.isStationary - - var locationAdded: Bool - locationAdded = addLocation(loc, smartPostion: enableSmartPosition) + let locationAdded = addLocation(loc, smartPostion: enableSmartPosition) if !isRecording && locationAdded { self.count = 1 } else if locationAdded && isRecording { @@ -81,18 +118,27 @@ import OSLog } } } + } catch is CancellationError { + // Handle explicit task cancellation gracefully. + Logger.services.info("📍 [App] Location updates task was cancelled.") } catch { + // Catch any other errors during location updates. Logger.services.error("💥 [App] Could not start location updates: \(error.localizedDescription, privacy: .public)") } - return + // The Task completes implicitly here. } } - + /// Stops receiving live location updates. func stopLocationUpdates() { Logger.services.info("🛑 [App] Stopping location updates") + // Setting `updatesStarted` to false will cause the `liveUpdates()` loop to break. self.updatesStarted = false } - + /// Adds a location to the array and updates tracking metrics, applying smart position filters if enabled. + /// - Parameters: + /// - location: The `CLLocation` object to add. + /// - smartPostion: A boolean indicating whether to apply smart position filtering. + /// - Returns: `true` if the location was added, `false` if it was filtered out by smart position. func addLocation(_ location: CLLocation, smartPostion: Bool) -> Bool { if smartPostion { let age = -location.timestamp.timeIntervalSinceNow @@ -104,6 +150,7 @@ import OSLog Logger.services.info("📍 [App] Smart Position - Bad Location: Horizontal Accuracy: \(location.horizontalAccuracy) \(location, privacy: .private(mask: .none))") return false } + // Consider adjusting this threshold based on your needs. 5 meters is quite strict. if location.horizontalAccuracy > 5 { Logger.services.info("📍 [App] Smart Position - Bad Location: Horizontal Accuracy: \(location.horizontalAccuracy) \(location, privacy: .private(mask: .none))") return false @@ -120,46 +167,56 @@ import OSLog } locationsArray.append(location) } else { + // If not recording, only keep the latest location. locationsArray = [location] } + // Store the last known location in UserDefaults for persistence. UserDefaults.standard.set(location.coordinate.latitude, forKey: "lastKnownLatitude") UserDefaults.standard.set(location.coordinate.longitude, forKey: "lastKnownLongitude") UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "lastKnownLocationTimestamp") return true } + // Default location (Apple Park) used as a fallback. static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090) + /// Provides the current location, falling back to last known or a default if necessary. static var currentLocation: CLLocationCoordinate2D { + // Attempt to get the most recent location from the manager. if let location = shared.manager.location { return location.coordinate } else { - // Check authorization status + // If manager.location is nil, check authorization status and potentially request. let status = shared.manager.authorizationStatus switch status { case .notDetermined: - Logger.services.info("📍 [App] Location permission not determined, requesting authorization") + Logger.services.info("📍 [App] Location permission not determined, requesting authorization (WhenInUse)") + // Requesting WhenInUse authorization here. For "Always" authorization, + // `requestLocationAlwaysPermissions()` should be called explicitly, + // typically from a user action or app setup. shared.manager.requestWhenInUseAuthorization() case .denied, .restricted: Logger.services.warning("📍 [App] Location access denied or restricted. Please enable location services in Settings to get accurate positioning!") + // Requesting WhenInUse authorization again, though user interaction is needed for denied/restricted. shared.manager.requestWhenInUseAuthorization() default: - break + break // For .authorizedAlways, .authorizedWhenInUse, .limited } - // Fallback 1: Last known location from UserDefaults (if within 4 hours) + // Fallback 1: Last known location from UserDefaults if it's recent (within 4 hours). if let lat = UserDefaults.standard.object(forKey: "lastKnownLatitude") as? Double, - let lon = UserDefaults.standard.object(forKey: "lastKnownLongitude") as? Double, - let timestamp = UserDefaults.standard.object(forKey: "lastKnownLocationTimestamp") as? Double, - lat >= -90 && lat <= 90, - lon >= -180 && lon <= 180, + let lon = UserDefaults.standard.object(forKey: "lastKnownLongitude") as? Double, + let timestamp = UserDefaults.standard.object(forKey: "lastKnownLocationTimestamp") as? Double, + lat >= -90 && lat <= 90, // Validate latitude + lon >= -180 && lon <= 180, // Validate longitude Date().timeIntervalSince1970 - timestamp <= 14_400 { // 4 hours in seconds Logger.services.info("📍 [App] Falling back to last known location (age: \(Int(Date().timeIntervalSince1970 - timestamp)) seconds)") return CLLocationCoordinate2D(latitude: lat, longitude: lon) } - // Fallback 2: Default location + // Fallback 2: Default location if no other location is available. Logger.services.warning("📍 [App] No Location and no last known location, something is really wrong. Teleporting user to Apple Park") return DefaultLocation } } - + /// Estimates the number of satellites in view based on horizontal and vertical accuracy. + /// This is a heuristic and not a direct report of satellite count. static var satsInView: Int { var sats = 0 if let newLocation = shared.locationsArray.last { @@ -185,5 +242,4 @@ import OSLog } return sats } - }