use std::time::Duration;
use objc::runtime::Object;
#[allow(unused_imports)]
use objc::{msg_send, sel, sel_impl};
use serde::{Deserialize, Serialize};
use tracing::{debug, info, warn};
fn objc_class(name: &str) -> *const objc::runtime::Class {
use std::ffi::CString;
let c = CString::new(name).unwrap_or_default();
unsafe { objc::runtime::objc_getClass(c.as_ptr()) }
}
fn ns_string_to_rust(ns: *mut Object) -> String {
if ns.is_null() {
return String::new();
}
let utf8: *const u8 = unsafe { msg_send![ns, UTF8String] };
if utf8.is_null() {
return String::new();
}
unsafe {
std::ffi::CStr::from_ptr(utf8 as *const std::ffi::c_char)
.to_string_lossy()
.into_owned()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Location {
pub latitude: f64,
pub longitude: f64,
pub accuracy_m: f64,
pub altitude: Option<f64>,
pub timestamp: String,
}
#[derive(Debug, thiserror::Error)]
pub enum LocationError {
#[error("Location Services disabled. Enable in System Settings > Privacy & Security > Location Services.")]
Disabled,
#[error("Location permission denied for this app. Grant access in System Settings > Privacy & Security > Location Services.")]
PermissionDenied,
#[error("Location request timed out after {0}s — try again or check GPS signal.")]
Timeout(u64),
#[error("CoreLocation error: {0}")]
Framework(String),
}
impl LocationError {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::Disabled => "location_disabled",
Self::PermissionDenied => "location_denied",
Self::Timeout(_) => "location_timeout",
Self::Framework(_) => "location_error",
}
}
}
pub fn request_location(timeout: Duration) -> Result<Location, LocationError> {
let cls = objc_class("CLLocationManager");
if cls.is_null() {
return Err(LocationError::Framework(
"CLLocationManager class not available".to_string(),
));
}
let enabled: bool = unsafe { msg_send![cls, locationServicesEnabled] };
if !enabled {
return Err(LocationError::Disabled);
}
let status: i32 = unsafe { msg_send![cls, authorizationStatus] };
debug!(status, "CLLocationManager authorization status");
match status {
1 | 2 => return Err(LocationError::PermissionDenied),
0 => {
info!("Location permission not determined — requesting authorization");
}
_ => {} }
let manager: *mut Object = unsafe {
let obj: *mut Object = msg_send![cls, alloc];
msg_send![obj, init]
};
if manager.is_null() {
return Err(LocationError::Framework(
"Failed to create CLLocationManager".to_string(),
));
}
unsafe {
let _: () = msg_send![manager, setDesiredAccuracy: -1.0f64]; }
if status == 0 {
unsafe {
let _: () = msg_send![manager, requestWhenInUseAuthorization];
}
extern "C" {
fn CFRunLoopRunInMode(mode: *const Object, seconds: f64, ret: bool) -> i32;
static kCFRunLoopDefaultMode: *const Object;
}
for _ in 0..20 {
unsafe {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false);
}
let new_status: i32 = unsafe { msg_send![cls, authorizationStatus] };
if new_status == 3 || new_status == 4 {
debug!(new_status, "Location authorization granted");
break;
}
if new_status == 1 || new_status == 2 {
return Err(LocationError::PermissionDenied);
}
}
}
unsafe {
let _: () = msg_send![manager, startUpdatingLocation];
}
extern "C" {
fn CFRunLoopRunInMode(mode: *const Object, seconds: f64, ret: bool) -> i32;
static kCFRunLoopDefaultMode: *const Object;
}
let deadline = std::time::Instant::now() + timeout;
loop {
let loc: *mut Object = unsafe { msg_send![manager, location] };
if !loc.is_null() {
let coord: (f64, f64) = unsafe { msg_send![loc, coordinate] };
let h_acc: f64 = unsafe { msg_send![loc, horizontalAccuracy] };
let alt: f64 = unsafe { msg_send![loc, altitude] };
let v_acc: f64 = unsafe { msg_send![loc, verticalAccuracy] };
if h_acc >= 0.0 {
unsafe {
let _: () = msg_send![manager, stopUpdatingLocation];
}
let ts: *mut Object = unsafe { msg_send![loc, timestamp] };
let desc: *mut Object = if ts.is_null() {
std::ptr::null_mut()
} else {
unsafe { msg_send![ts, description] }
};
let timestamp = ns_string_to_rust(desc);
return Ok(Location {
latitude: coord.0,
longitude: coord.1,
accuracy_m: h_acc,
altitude: if v_acc >= 0.0 { Some(alt) } else { None },
timestamp,
});
}
}
if std::time::Instant::now() >= deadline {
unsafe {
let _: () = msg_send![manager, stopUpdatingLocation];
}
warn!("Location request timed out");
return Err(LocationError::Timeout(timeout.as_secs()));
}
unsafe {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.2, false);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn location_error_codes() {
assert_eq!(LocationError::Disabled.code(), "location_disabled");
assert_eq!(LocationError::PermissionDenied.code(), "location_denied");
assert_eq!(LocationError::Timeout(10).code(), "location_timeout");
assert_eq!(
LocationError::Framework("test".to_string()).code(),
"location_error"
);
}
#[test]
fn location_serializes() {
let loc = Location {
latitude: 60.1699,
longitude: 24.9384,
accuracy_m: 10.0,
altitude: Some(15.0),
timestamp: "2026-03-22T12:00:00Z".to_string(),
};
let json = serde_json::to_string(&loc).unwrap();
assert!(json.contains("60.1699"));
assert!(json.contains("24.9384"));
}
}