ashpd/desktop/
usb.rs

1// Copyright (C) 2024-2025 GNOME Foundation
2//
3// Authors:
4//     Hubert Figuière <hub@figuiere.net>
5//
6
7//! Provide an interface to USB device. Allow enumerating devices
8//! and requiring access to.
9//! ```rust,no_run
10//! use ashpd::desktop::usb::UsbProxy;
11//! use futures_util::StreamExt;
12//!
13//! async fn watch_devices() -> ashpd::Result<()> {
14//!     let usb = UsbProxy::new().await?;
15//!     let session = usb.create_session().await?;
16//!     if let Some(response) = usb.receive_device_events().await?.next().await {
17//!         let events = response.events();
18//!         for ev in events {
19//!             println!(
20//!                 "Received event: {} for device {}",
21//!                 ev.action(),
22//!                 ev.device_id()
23//!             );
24//!         }
25//!     }
26//!
27//!     Ok(())
28//! }
29//! ```
30
31use std::collections::HashMap;
32
33use futures_util::Stream;
34use serde::Deserialize;
35use zbus::zvariant::{
36    DeserializeDict, ObjectPath, OwnedFd, OwnedObjectPath, OwnedValue, SerializeDict, Type, Value,
37};
38
39use crate::{
40    desktop::{HandleToken, Session, SessionPortal},
41    proxy::Proxy,
42    Error, WindowIdentifier,
43};
44
45#[derive(Debug, SerializeDict, Type, Default)]
46#[zvariant(signature = "dict")]
47struct CreateSessionOptions {
48    handle_token: HandleToken,
49    session_handle_token: HandleToken,
50}
51
52/// Options for the USB portal.
53#[derive(SerializeDict, Type, Debug, Default)]
54#[zvariant(signature = "dict")]
55struct UsbEnumerateOptions {}
56
57/// USB device description
58#[derive(SerializeDict, DeserializeDict, Type, Debug, Default)]
59#[zvariant(signature = "dict")]
60pub struct UsbDevice {
61    parent: Option<String>,
62    /// Device can be opened for reading. Default is false.
63    readable: Option<bool>,
64    /// Device can be opened for writing. Default is false.
65    writable: Option<bool>,
66    /// The device node for the USB.
67    #[zvariant(rename = "device-file")]
68    device_file: Option<String>,
69    /// Device properties
70    properties: Option<HashMap<String, OwnedValue>>,
71}
72
73impl UsbDevice {
74    /// Device ID of the parent device
75    pub fn parent(&self) -> Option<&str> {
76        self.parent.as_deref()
77    }
78
79    /// Return if the device is readable.
80    pub fn is_readable(&self) -> bool {
81        self.readable.unwrap_or(false)
82    }
83
84    /// Return if the device is writable.
85    pub fn is_writable(&self) -> bool {
86        self.writable.unwrap_or(false)
87    }
88
89    /// Return the optional device file.
90    pub fn device_file(&self) -> Option<&str> {
91        self.device_file.as_deref()
92    }
93
94    /// Return the vendor string property for display.
95    pub fn vendor(&self) -> Option<String> {
96        self.properties.as_ref().and_then(|properties| {
97            properties
98                .get("ID_VENDOR_FROM_DATABASE")
99                .or_else(|| properties.get("ID_VENDOR_ENC"))
100                .and_then(|v| v.downcast_ref::<String>().ok())
101        })
102    }
103
104    /// Return the model string property for display.
105    pub fn model(&self) -> Option<String> {
106        self.properties.as_ref().and_then(|properties| {
107            properties
108                .get("ID_MODEL_FROM_DATABASE")
109                .or_else(|| properties.get("ID_MODEL_ENC"))
110                .and_then(|v| v.downcast_ref::<String>().ok())
111        })
112    }
113
114    /// Return the device properties.
115    pub fn properties(&self) -> Option<&HashMap<String, OwnedValue>> {
116        self.properties.as_ref()
117    }
118}
119
120/// USB error for acquiring device.
121#[derive(Debug)]
122pub struct UsbError(pub Option<String>);
123
124impl std::fmt::Display for UsbError {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        write!(f, "{}", self.0.as_deref().unwrap_or(""))
127    }
128}
129
130impl std::error::Error for UsbError {}
131
132impl From<AcquiredDevice> for Result<OwnedFd, UsbError> {
133    fn from(v: AcquiredDevice) -> Result<OwnedFd, UsbError> {
134        if let Some(fd) = v.fd {
135            if v.success {
136                Ok(fd)
137            } else {
138                Err(UsbError(v.error))
139            }
140        } else {
141            Err(UsbError(None))
142        }
143    }
144}
145
146/// Device to acquire.
147#[derive(SerializeDict, Type, Debug, Default)]
148#[zvariant(signature = "dict")]
149struct AcquireDevice {
150    writable: bool,
151}
152
153/// Device to acquire. Tuple with the device ID and whether the
154/// requested permission should be to write.
155pub struct Device(String /* ID */, bool /* writable */);
156
157impl Device {
158    /// Create a new `Device`.
159    pub fn new(id: String, writable: bool) -> Device {
160        Device(id, writable)
161    }
162
163    /// Get the device ID.
164    pub fn id(&self) -> &str {
165        &self.0
166    }
167
168    /// Return whether the device is writable.
169    pub fn is_writable(&self) -> bool {
170        self.1
171    }
172}
173
174/// Option for AcquireDevice call.
175#[derive(SerializeDict, Type, Debug, Default)]
176#[zvariant(signature = "dict")]
177struct AcquireOptions {
178    handle_token: HandleToken,
179}
180
181/// Finished device acquired.
182#[derive(DeserializeDict, Type, Debug, Default)]
183#[zvariant(signature = "dict")]
184struct AcquiredDevice {
185    success: bool,
186    fd: Option<OwnedFd>,
187    error: Option<String>,
188}
189
190#[derive(Debug, Deserialize, Type)]
191/// A USB event received part of the `device_event` signal response.
192/// An event is composed of an `action`, a device `id` and the
193/// [`UsbDevice`] description.
194pub struct UsbEvent(/* action */ String, /* id */ String, UsbDevice);
195
196impl UsbEvent {
197    /// The action. Possible values are `"add"`, `"change"` or `"remove"`.
198    pub fn action(&self) -> &str {
199        &self.0
200    }
201
202    /// The device ID string.
203    pub fn device_id(&self) -> &str {
204        &self.1
205    }
206
207    /// The [`UsbDevice`] properties.
208    pub fn device(&self) -> &UsbDevice {
209        &self.2
210    }
211}
212
213#[derive(Debug, Deserialize, Type)]
214/// A response received when the `device_event` signal is received.
215pub struct UsbDeviceEvent(OwnedObjectPath, Vec<UsbEvent>);
216
217impl UsbDeviceEvent {
218    /// The session that triggered the state change
219    pub fn session_handle(&self) -> ObjectPath<'_> {
220        self.0.as_ref()
221    }
222
223    /// Events received
224    pub fn events(&self) -> &[UsbEvent] {
225        &self.1
226    }
227}
228
229/// This interface provides access to USB devices.
230#[derive(Debug)]
231#[doc(alias = "org.freedesktop.portal.Usb")]
232pub struct UsbProxy<'a>(Proxy<'a>);
233
234impl<'a> UsbProxy<'a> {
235    /// Create a new instance of [`UsbProxy`].
236    pub async fn new() -> Result<UsbProxy<'a>, Error> {
237        let proxy = Proxy::new_desktop("org.freedesktop.portal.Usb").await?;
238        Ok(Self(proxy))
239    }
240
241    /// Create a USB session.
242    ///
243    /// While this session is active, the caller will receive
244    /// `DeviceEvents` signals with device addition and removal.
245    ///
246    /// # Specifications
247    ///
248    /// See also [`CreateSession`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Usb.html#org-freedesktop-portal-usb-createsession).
249    #[doc(alias = "CreateSession")]
250    pub async fn create_session(&self) -> Result<Session<'a, Self>, Error> {
251        let options = CreateSessionOptions::default();
252        let session: OwnedObjectPath = self.0.call("CreateSession", &(&options)).await?;
253        Session::new(session).await
254    }
255
256    /// Enumerate USB devices.
257    ///
258    /// Return a vector of tuples with the device ID and the [`UsbDevice`]
259    ///
260    /// # Specifications
261    ///
262    /// See also [`EnumerateDevices`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Usb.html#org-freedesktop-portal-usb-enumeratedevices).
263    #[doc(alias = "EnumerateDevices")]
264    pub async fn enumerate_devices(&self) -> Result<Vec<(String, UsbDevice)>, Error> {
265        let options = UsbEnumerateOptions::default();
266        self.0.call("EnumerateDevices", &(&options)).await
267    }
268
269    /// Acquire devices
270    ///
271    /// The portal will perform the permission request. In case of
272    /// success, ie permission is granted, it returns a vector of
273    /// tuples containing the device ID and the file descriptor or
274    /// error.
275    ///
276    /// # Specifications
277    ///
278    /// See also [`AcquireDevices`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Usb.html#org-freedesktop-portal-usb-acquiredevices).
279    #[doc(alias = "AcquireDevices")]
280    pub async fn acquire_devices(
281        &self,
282        parent_window: Option<&WindowIdentifier>,
283        devices: &[Device],
284    ) -> Result<Vec<(String, Result<OwnedFd, UsbError>)>, Error> {
285        let options = AcquireOptions::default();
286        let parent_window = parent_window.map(|i| i.to_string()).unwrap_or_default();
287        let acquire_devices: Vec<(String, AcquireDevice)> = devices
288            .iter()
289            .map(|dev| {
290                let device = AcquireDevice { writable: dev.1 };
291                (dev.0.to_string(), device)
292            })
293            .collect();
294        let request = self
295            .0
296            .empty_request(
297                &options.handle_token,
298                "AcquireDevices",
299                &(&parent_window, &acquire_devices, &options),
300            )
301            .await?;
302        let mut devices: Vec<(String, Result<OwnedFd, UsbError>)> = vec![];
303        if request.response().is_ok() {
304            let path = request.path();
305            loop {
306                let (mut new_devices, finished) = self.finish_acquire_devices(path).await?;
307                devices.append(&mut new_devices);
308                if finished {
309                    break;
310                }
311            }
312        }
313        Ok(devices)
314    }
315
316    /// Call on success of acquire_devices. This never need to be called
317    /// by client applications.
318    ///
319    /// # Specifications
320    ///
321    /// See also [`FinishAcquireDevices`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Usb.html#org-freedesktop-portal-usb-finishacquiredevices).
322    #[doc(alias = "FinishAcquireDevices")]
323    async fn finish_acquire_devices(
324        &self,
325        request_path: &ObjectPath<'_>,
326    ) -> Result<(Vec<(String, Result<OwnedFd, UsbError>)>, bool), Error> {
327        let options: HashMap<&str, Value<'_>> = HashMap::new();
328        self.0
329            .call("FinishAcquireDevices", &(request_path, &options))
330            .await
331            .map(|result: (Vec<(String, AcquiredDevice)>, bool)| {
332                let finished = result.1;
333                (
334                    result
335                        .0
336                        .into_iter()
337                        .map(|item| (item.0, item.1.into()))
338                        .collect::<Vec<_>>(),
339                    finished,
340                )
341            })
342    }
343
344    /// Release devices
345    ///
346    /// Release all the devices whose ID is specified in `devices`.
347    ///
348    /// # Specifications
349    ///
350    /// See also [`ReleaseDevices`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Usb.html#org-freedesktop-portal-usb-releasedevices).
351    #[doc(alias = "ReleaseDevices")]
352    pub async fn release_devices(&self, devices: &[&str]) -> Result<(), Error> {
353        let options: HashMap<&str, Value<'_>> = HashMap::new();
354        self.0.call("ReleaseDevices", &(devices, &options)).await
355    }
356
357    /// Signal emitted on a device event
358    ///
359    /// Will emit [`UsbDeviceEvent`].
360    ///
361    /// # Specifications
362    ///
363    /// See also [`DeviceEvents`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Usb.html#org-freedesktop-portal-usb-deviceevents).
364    #[doc(alias = "DeviceEvents")]
365    pub async fn receive_device_events(&self) -> Result<impl Stream<Item = UsbDeviceEvent>, Error> {
366        self.0.signal("DeviceEvents").await
367    }
368}
369
370impl crate::Sealed for UsbProxy<'_> {}
371impl SessionPortal for UsbProxy<'_> {}