fractal/utils/location/
linux.rs1use 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#[derive(Debug, Default)]
18pub(crate) struct LinuxLocation {
19 inner: OnceCell<Arc<ProxyAndSession>>,
20}
21
22#[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 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 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 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}