fractal/utils/location/
linux.rs

1//! Linux Location API.
2
3use std::{cell::OnceCell, sync::Arc};
4
5use ashpd::desktop::{
6    location::{Accuracy, Location as PortalLocation, LocationProxy},
7    Session,
8};
9use futures_util::{future, stream, FutureExt, Stream, StreamExt, TryFutureExt};
10use geo_uri::GeoUri;
11use tracing::error;
12
13use super::{LocationError, LocationExt};
14use crate::spawn_tokio;
15
16/// Location API under Linux, using the Location XDG Desktop Portal.
17#[derive(Debug, Default)]
18pub(crate) struct LinuxLocation {
19    inner: OnceCell<Arc<ProxyAndSession>>,
20}
21
22/// A location proxy and it's associated session.
23#[derive(Debug)]
24struct ProxyAndSession {
25    proxy: LocationProxy<'static>,
26    session: Session<'static, LocationProxy<'static>>,
27}
28
29impl LocationExt for LinuxLocation {
30    fn is_available(&self) -> bool {
31        true
32    }
33
34    async fn init(&self) -> Result<(), LocationError> {
35        match self.init().await {
36            Ok(()) => Ok(()),
37            Err(error) => {
38                error!("Could not initialize location API: {error}");
39                Err(error.into())
40            }
41        }
42    }
43
44    async fn updates_stream(&self) -> Result<impl Stream<Item = GeoUri> + '_, LocationError> {
45        match self.updates_stream().await {
46            Ok(stream) => Ok(stream.map(|l| {
47                GeoUri::builder()
48                    .latitude(l.latitude())
49                    .longitude(l.longitude())
50                    .build()
51                    .expect("Got invalid coordinates from location API")
52            })),
53            Err(error) => {
54                error!("Could not access update stream of location API: {error}");
55                Err(error.into())
56            }
57        }
58    }
59}
60
61impl LinuxLocation {
62    pub(crate) fn new() -> Self {
63        Self::default()
64    }
65
66    /// Initialize the proxy.
67    async fn init(&self) -> Result<(), ashpd::Error> {
68        if self.inner.get().is_some() {
69            return Ok(());
70        }
71
72        let inner = spawn_tokio!(async move {
73            let proxy = LocationProxy::new().await?;
74
75            let session = proxy
76                .create_session(Some(0), Some(0), Some(Accuracy::Exact))
77                .await?;
78
79            ashpd::Result::Ok(ProxyAndSession { proxy, session })
80        })
81        .await
82        .unwrap()?;
83
84        self.inner.set(inner.into()).unwrap();
85        Ok(())
86    }
87
88    /// Listen to updates from the proxy.
89    async fn updates_stream(
90        &self,
91    ) -> Result<impl Stream<Item = PortalLocation> + '_, ashpd::Error> {
92        let inner = self
93            .inner
94            .get()
95            .expect("location API should be initialized")
96            .clone();
97
98        spawn_tokio!(async move {
99            let ProxyAndSession { proxy, session } = &*inner;
100
101            // We want to be listening for new locations whenever the session is up
102            // otherwise we might lose the first response and will have to wait for a future
103            // update by geoclue.
104            let mut stream = proxy.receive_location_updated().await?;
105            let (_, first_location) = future::try_join(
106                proxy.start(session, None).into_future(),
107                stream.next().map(|l| l.ok_or(ashpd::Error::NoResponse)),
108            )
109            .await?;
110
111            ashpd::Result::Ok(stream::once(future::ready(first_location)).chain(stream))
112        })
113        .await
114        .unwrap()
115    }
116}
117
118impl Drop for LinuxLocation {
119    fn drop(&mut self) {
120        if let Some(inner) = self.inner.take() {
121            spawn_tokio!(async move {
122                if let Err(error) = inner.session.close().await {
123                    error!("Could not close session of location API: {error}");
124                }
125            });
126        }
127    }
128}
129
130impl From<ashpd::Error> for LocationError {
131    fn from(value: ashpd::Error) -> Self {
132        match value {
133            ashpd::Error::Response(ashpd::desktop::ResponseError::Cancelled) => Self::Cancelled,
134            ashpd::Error::Portal(ashpd::PortalError::NotAllowed(_)) => Self::Disabled,
135            _ => Self::Other,
136        }
137    }
138}