use std::future::Future;
use std::time::Duration;
use serde_json::json;
use windows::Devices::Geolocation::{
GeolocationAccessStatus, Geolocator, Geoposition, PositionAccuracy, PositionStatus,
};
use windows::Win32::Foundation::E_ACCESSDENIED;
use super::Platform;
use crate::error::PlatformError;
use crate::traits::location::{Location, LocationRequestConfig};
const CODE_PERMISSION_DENIED: u32 = 3002;
const CODE_SERVICES_DISABLED: u32 = 4001;
const CODE_TIMEOUT: u32 = 5002;
impl Location for Platform {
fn is_location_enabled(&self) -> Result<bool, PlatformError> {
let geolocator = Geolocator::new().map_err(|err| {
PlatformError::Platform(format!("failed to create Geolocator: {err}"))
})?;
let status = geolocator.LocationStatus().map_err(|err| {
PlatformError::Platform(format!("failed to query location status: {err}"))
})?;
Ok(status != PositionStatus::Disabled && status != PositionStatus::NotAvailable)
}
fn request_location(
&self,
config: LocationRequestConfig,
) -> impl Future<Output = Result<String, PlatformError>> + Send {
async move {
match config.high_accuracy_expire_time {
Some(timeout_ms) if timeout_ms > 0 => tokio::time::timeout(
Duration::from_millis(u64::from(timeout_ms)),
request_location_once(config),
)
.await
.map_err(|_| PlatformError::BusinessError(CODE_TIMEOUT))?,
_ => request_location_once(config).await,
}
}
}
}
async fn request_location_once(config: LocationRequestConfig) -> Result<String, PlatformError> {
match Geolocator::RequestAccessAsync() {
Ok(operation) => match operation.await {
Ok(status) if status == GeolocationAccessStatus::Denied => {
return Err(PlatformError::BusinessError(CODE_PERMISSION_DENIED));
}
Ok(_) => {}
Err(err) => log::warn!(
"Windows Location: RequestAccessAsync failed ({err}); attempting lookup anyway"
),
},
Err(err) => log::warn!(
"Windows Location: RequestAccessAsync unavailable ({err}); attempting lookup anyway"
),
}
let geolocator = Geolocator::new()
.map_err(|err| PlatformError::Platform(format!("failed to create Geolocator: {err}")))?;
let accuracy = if config.is_high_accuracy {
PositionAccuracy::High
} else {
PositionAccuracy::Default
};
if let Err(err) = geolocator.SetDesiredAccuracy(accuracy) {
log::warn!("Windows Location: SetDesiredAccuracy failed: {err}");
}
if matches!(geolocator.LocationStatus(), Ok(status) if status == PositionStatus::Disabled) {
return Err(PlatformError::BusinessError(CODE_SERVICES_DISABLED));
}
let operation = geolocator
.GetGeopositionAsync()
.map_err(map_geoposition_error)?;
let position = operation.await.map_err(map_geoposition_error)?;
build_location_payload(&position, config.include_altitude)
}
fn map_geoposition_error(err: windows::core::Error) -> PlatformError {
if err.code() == E_ACCESSDENIED {
PlatformError::BusinessError(CODE_PERMISSION_DENIED)
} else {
PlatformError::Platform(format!("location request failed: {err}"))
}
}
fn build_location_payload(
position: &Geoposition,
include_altitude: bool,
) -> Result<String, PlatformError> {
let coordinate = position.Coordinate().map_err(|err| {
PlatformError::Platform(format!("location result missing coordinate: {err}"))
})?;
let latitude = coordinate
.Latitude()
.map_err(|err| PlatformError::Platform(format!("failed to read latitude: {err}")))?;
let longitude = coordinate
.Longitude()
.map_err(|err| PlatformError::Platform(format!("failed to read longitude: {err}")))?;
let horizontal_accuracy = sanitize_measurement(coordinate.Accuracy().unwrap_or(0.0));
let vertical_accuracy = sanitize_measurement(coordinate.AltitudeAccuracy().unwrap_or(0.0));
let speed = sanitize_measurement(coordinate.Speed().unwrap_or(0.0));
let altitude = if include_altitude {
sanitize_measurement(coordinate.Altitude().unwrap_or(0.0))
} else {
0.0
};
Ok(json!({
"latitude": latitude,
"longitude": longitude,
"speed": speed,
"accuracy": horizontal_accuracy,
"altitude": altitude,
"vertical_accuracy": vertical_accuracy,
"horizontal_accuracy": horizontal_accuracy,
})
.to_string())
}
fn sanitize_measurement(value: f64) -> f64 {
if value.is_finite() && value >= 0.0 {
value
} else {
0.0
}
}