Skip to main content

eventkit/
location.rs

1//! CoreLocation integration for getting the user's current location.
2//!
3//! Uses `CLLocationManager` to request a one-shot location fix.
4//! Requires the `location` feature and `NSLocationWhenInUseUsageDescription` in Info.plist.
5
6use crate::{EventKitError, Result};
7use objc2::rc::Retained;
8use objc2_core_location::{CLLocationCoordinate2D, CLLocationManager};
9use std::time::Duration;
10
11/// Authorization status for location services.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum LocationAuthorizationStatus {
14    NotDetermined,
15    Restricted,
16    Denied,
17    Authorized,
18}
19
20/// A location coordinate (latitude, longitude).
21#[derive(Debug, Clone, Copy)]
22pub struct Coordinate {
23    pub latitude: f64,
24    pub longitude: f64,
25}
26
27/// Manages CoreLocation access for getting the user's current location.
28///
29/// CLLocationManager is `!Send + !Sync` — create instances on the thread
30/// where you need them (same pattern as EventKit managers).
31pub struct LocationManager {
32    manager: Retained<CLLocationManager>,
33}
34
35impl LocationManager {
36    /// Create a new LocationManager.
37    pub fn new() -> Self {
38        let manager = unsafe { CLLocationManager::new() };
39        Self { manager }
40    }
41
42    /// Check the current authorization status for location services.
43    pub fn authorization_status(&self) -> LocationAuthorizationStatus {
44        let status = unsafe { self.manager.authorizationStatus() };
45        // CLAuthorizationStatus is a newtype around i32:
46        // 0 = notDetermined, 1 = restricted, 2 = denied,
47        // 3 = authorizedAlways, 4 = authorizedWhenInUse
48        match status.0 {
49            0 => LocationAuthorizationStatus::NotDetermined,
50            1 => LocationAuthorizationStatus::Restricted,
51            2 => LocationAuthorizationStatus::Denied,
52            3 | 4 => LocationAuthorizationStatus::Authorized,
53            _ => LocationAuthorizationStatus::Denied,
54        }
55    }
56
57    /// Request when-in-use authorization for location services.
58    ///
59    /// On macOS, this shows the system permission dialog. The result is
60    /// available via `authorization_status()` after the user responds.
61    pub fn request_when_in_use_authorization(&self) {
62        unsafe {
63            self.manager.requestWhenInUseAuthorization();
64        }
65    }
66
67    /// Get the most recently cached location without starting updates.
68    ///
69    /// Returns `None` if no location has been determined yet.
70    pub fn cached_location(&self) -> Option<Coordinate> {
71        let location = unsafe { self.manager.location() }?;
72        let coord: CLLocationCoordinate2D = unsafe { location.coordinate() };
73        Some(Coordinate {
74            latitude: coord.latitude,
75            longitude: coord.longitude,
76        })
77    }
78
79    /// Request a fresh location fix synchronously (blocks until result or timeout).
80    ///
81    /// Starts location updates, waits for a result, then stops updates.
82    /// Times out after `timeout` duration.
83    pub fn get_current_location(&self, timeout: Duration) -> Result<Coordinate> {
84        match self.authorization_status() {
85            LocationAuthorizationStatus::NotDetermined => {
86                self.request_when_in_use_authorization();
87                // Give the system a moment to process the authorization request
88                std::thread::sleep(Duration::from_millis(500));
89                if self.authorization_status() != LocationAuthorizationStatus::Authorized {
90                    return Err(EventKitError::AuthorizationDenied);
91                }
92            }
93            LocationAuthorizationStatus::Denied => {
94                return Err(EventKitError::AuthorizationDenied);
95            }
96            LocationAuthorizationStatus::Restricted => {
97                return Err(EventKitError::AuthorizationRestricted);
98            }
99            LocationAuthorizationStatus::Authorized => {}
100        }
101
102        // Start location updates and poll for a result
103        unsafe {
104            self.manager.startUpdatingLocation();
105        }
106
107        // Poll for location (CLLocationManager updates are delivered on the
108        // run loop; we poll the cached location property)
109        let start = std::time::Instant::now();
110        let result = loop {
111            if let Some(location) = self.cached_location() {
112                break Some(location);
113            }
114
115            if start.elapsed() >= timeout {
116                break None;
117            }
118
119            // Brief sleep to avoid busy-waiting
120            std::thread::sleep(Duration::from_millis(100));
121        };
122
123        unsafe {
124            self.manager.stopUpdatingLocation();
125        }
126
127        result.ok_or_else(|| EventKitError::EventKitError("Location request timed out".into()))
128    }
129}
130
131impl Default for LocationManager {
132    fn default() -> Self {
133        Self::new()
134    }
135}