Skip to main content

mtp_rs/mtp/
device.rs

1//! MtpDevice - the main entry point for MTP operations.
2
3use crate::mtp::backend::usb::UsbBackend;
4use crate::mtp::backend::{Backend, MtpBackend};
5use crate::mtp::{Capabilities, DeviceEvent, DeviceInfo, Error, Storage, StorageId};
6use crate::ptp::PtpSession;
7use crate::transport::{NusbTransport, Transport};
8use std::sync::Arc;
9use std::time::Duration;
10
11/// An MTP device connection.
12///
13/// This is the main entry point for interacting with MTP devices.
14/// Use `MtpDevice::open_first()` to connect to the first available device,
15/// or `MtpDevice::builder()` for more control.
16///
17/// The device is a thin façade over a backend-neutral implementation (the internal `MtpBackend`
18/// trait). Today the only backend is PTP-over-USB (which also drives the virtual and mock
19/// transports); a Windows WPD backend is planned. Consumers work against the neutral
20/// [`crate::mtp`] types throughout.
21///
22/// # Example
23///
24/// ```rust,no_run
25/// use mtp_rs::mtp::MtpDevice;
26///
27/// # async fn example() -> Result<(), mtp_rs::Error> {
28/// // Open the first MTP device
29/// let device = MtpDevice::open_first().await?;
30///
31/// println!("Connected to: {} {}",
32///          device.device_info().manufacturer,
33///          device.device_info().model);
34///
35/// // Get storages
36/// for storage in device.storages().await? {
37///     println!("Storage: {} ({} free)",
38///              storage.info().description,
39///              storage.info().free_space);
40/// }
41/// # Ok(())
42/// # }
43/// ```
44#[derive(Clone)]
45pub struct MtpDevice {
46    pub(crate) backend: Arc<dyn MtpBackend>,
47}
48
49impl MtpDevice {
50    /// Create a builder for configuring device options.
51    pub fn builder() -> MtpDeviceBuilder {
52        MtpDeviceBuilder::new()
53    }
54
55    /// Open the first available MTP device with default settings.
56    pub async fn open_first() -> Result<Self, Error> {
57        Self::builder().open_first().await
58    }
59
60    /// Open a device at a specific USB location (port) with default settings.
61    ///
62    /// Use `list_devices()` to get available location IDs.
63    pub async fn open_by_location(location_id: u64) -> Result<Self, Error> {
64        Self::builder().open_by_location(location_id).await
65    }
66
67    /// Open a device by its serial number with default settings.
68    ///
69    /// This identifies a specific physical device regardless of which USB port
70    /// it's connected to.
71    pub async fn open_by_serial(serial: &str) -> Result<Self, Error> {
72        Self::builder().open_by_serial(serial).await
73    }
74
75    /// Reset the USB transport state of the device with this serial, without
76    /// opening a session.
77    ///
78    /// Sends the USB Still Image Class Device Reset request (`bRequest=0x66`),
79    /// clears halted bulk endpoints, and drains stale bulk data. This is the USB
80    /// **transport-level** reset, not the in-session `ResetDevice` (0x1010) PTP
81    /// operation: it works precisely when the device is too confused for PTP
82    /// traffic, which is when you need it.
83    ///
84    /// # Warning: on Android this can break MTP until the user replugs
85    ///
86    /// **Treat this as a last resort, not a recovery step.** Sending the reset to
87    /// a *healthy* Pixel 9 Pro XL permanently killed its MTP function: Android's
88    /// `MtpServer` lost its endpoint read (`ECANCELED`, then `EPIPE`) and never
89    /// re-armed, while the USB device controller stayed `configured`. The phone
90    /// kept enumerating and kept showing up in a device list, and answered
91    /// nothing. Ten spaced reopens over ~100 s all timed out; only a physical
92    /// unplug and replug brought it back (verified on a Pixel 9 Pro XL,
93    /// macOS/nusb + `adb logcat`, 2026-07-21).
94    ///
95    /// Android is the most common MTP device class, so reach for this only after
96    /// spaced reopens have already failed, or on a device that's *already*
97    /// unreachable, where you can't make things much worse. See
98    /// `docs/notes/android-wedges-and-the-reset-kill-switch.md`.
99    ///
100    /// # Why this isn't a method on an open device
101    ///
102    /// It only claims the USB interface and stops there. The regular opens run
103    /// `OpenSession` + `GetDeviceInfo`, which is exactly what a wedged device
104    /// can't answer, so a reset hanging off an already-open [`MtpDevice`] would
105    /// be useless in the case it exists for. **Drop your device first**: holding
106    /// it keeps the interface claimed, and this call would then fail to claim it.
107    /// You have to reopen afterwards regardless, since the PTP session is gone.
108    ///
109    /// # Recovering a wedged device
110    ///
111    /// After [`Error::DeviceReset`], when an operation hangs and never returns
112    /// (the Android signature: no error at all), or when every operation fails
113    /// with "Transaction ID mismatch" / "expected Response container type":
114    ///
115    /// 1. Drop the [`MtpDevice`] (and any [`Storage`] handles).
116    /// 2. Wait a few seconds **quiet**, with no USB traffic at all.
117    /// 3. Reopen with idle-spaced retries, several of them.
118    /// 4. Only if every reopen failed, and knowing the Android warning above,
119    ///    call this and then repeat steps 2 and 3.
120    ///
121    /// Step 3 is where consumers go wrong: don't try once and give up, and don't
122    /// hammer close/open in a tight loop (that keeps the device busy and
123    /// re-wedges it into a hard `Timeout`). Expect the early attempts to fail.
124    /// A Pixel's wedge cleared on a fresh open with no reset at all (verified on
125    /// a Pixel 9 Pro XL, macOS/nusb, 2026-07-20). On a Galaxy S23 Ultra the
126    /// observed sequence was reset, then a reopen returning `Timeout`, then one
127    /// returning `SessionAlreadyOpen`, then success (verified on SM-S918B,
128    /// macOS/nusb, 2026-07-20); the control without a reset was never run, so
129    /// it's unknown whether spaced reopens alone would have sufficed there.
130    ///
131    /// # Errors
132    ///
133    /// [`Error::NoDevice`] when no USB device has that serial, and
134    /// [`Error::Unsupported`] for a virtual device, which is a filesystem with no
135    /// USB transport to reset.
136    pub async fn reset_by_serial(serial: &str) -> Result<(), Error> {
137        Self::builder().reset_by_serial(serial).await
138    }
139
140    /// Reset the USB transport state of the device at this location, without
141    /// opening a session.
142    ///
143    /// See [`reset_by_serial`](Self::reset_by_serial) for the full contract and
144    /// the recovery sequence to follow.
145    ///
146    /// **Last resort on Android**: the reset can break the phone's MTP function
147    /// until the user physically replugs. Try spaced reopens first.
148    pub async fn reset_by_location(location_id: u64) -> Result<(), Error> {
149        Self::builder().reset_by_location(location_id).await
150    }
151
152    /// Reset the USB transport state of the first available device, without
153    /// opening a session.
154    ///
155    /// See [`reset_by_serial`](Self::reset_by_serial) for the full contract and
156    /// the recovery sequence to follow.
157    ///
158    /// **Last resort on Android**: the reset can break the phone's MTP function
159    /// until the user physically replugs. Try spaced reopens first.
160    pub async fn reset_first() -> Result<(), Error> {
161        Self::builder().reset_first().await
162    }
163
164    /// List all available MTP devices without opening them.
165    pub fn list_devices() -> Result<Vec<MtpDeviceInfo>, Error> {
166        Self::list_devices_with_known(&[])
167    }
168
169    /// List all available MTP devices, including additional devices identified
170    /// by the given VID/PID pairs.
171    ///
172    /// Devices matching the provided VID/PID pairs are included in the results
173    /// even if their USB descriptors don't match standard MTP class codes. This
174    /// is useful for legacy or otherwise unusual devices with non-standard USB
175    /// descriptors that still speak MTP.
176    ///
177    /// # Example
178    ///
179    /// ```rust,no_run
180    /// use mtp_rs::mtp::MtpDevice;
181    ///
182    /// let devices = MtpDevice::list_devices_with_known(&[
183    ///     (0x045E, 0x0710), // custom VID/PID
184    /// ])?;
185    /// for d in &devices {
186    ///     println!("{:04x}:{:04x} {}", d.vendor_id, d.product_id,
187    ///              d.product.as_deref().unwrap_or("unknown"));
188    /// }
189    /// # Ok::<(), mtp_rs::Error>(())
190    /// ```
191    pub fn list_devices_with_known(known: &[(u16, u16)]) -> Result<Vec<MtpDeviceInfo>, Error> {
192        let devices = NusbTransport::list_mtp_devices_with_known(known)?;
193        #[allow(unused_mut)]
194        let mut result: Vec<MtpDeviceInfo> =
195            devices.into_iter().map(MtpDeviceInfo::from_usb).collect();
196
197        #[cfg(feature = "virtual-device")]
198        result.extend(crate::transport::virtual_device::registry::list_virtual_devices());
199
200        Ok(result)
201    }
202
203    /// Get device information (backend-neutral identity).
204    #[must_use]
205    pub fn device_info(&self) -> &DeviceInfo {
206        self.backend.device_info()
207    }
208
209    /// What this device supports (backend-neutral capabilities).
210    ///
211    /// Replaces the old per-operation accessors. Advertised support can still be wrong on some
212    /// devices (see the Fujifilm quirk in `AGENTS.md`), so treat these as a strong hint.
213    #[must_use]
214    pub fn capabilities(&self) -> &Capabilities {
215        self.backend.capabilities()
216    }
217
218    /// Whether the device supports renaming objects.
219    ///
220    /// Convenience over [`capabilities()`](Self::capabilities)`.can_rename`.
221    #[must_use]
222    pub fn supports_rename(&self) -> bool {
223        self.backend.capabilities().can_rename
224    }
225
226    /// Whether the device supports creating objects (uploads and folders).
227    ///
228    /// Convenience over [`capabilities()`](Self::capabilities)`.can_upload`.
229    #[must_use]
230    pub fn supports_upload(&self) -> bool {
231        self.backend.capabilities().can_upload
232    }
233
234    /// Get all storages on the device.
235    pub async fn storages(&self) -> Result<Vec<Storage>, Error> {
236        let infos = self.backend.storages().await?;
237        Ok(infos
238            .into_iter()
239            .map(|info| Storage::new(Arc::clone(&self.backend), info.id, info))
240            .collect())
241    }
242
243    /// Get a specific storage by ID.
244    pub async fn storage(&self, id: StorageId) -> Result<Storage, Error> {
245        let info = self.backend.storage_info(id).await?;
246        Ok(Storage::new(Arc::clone(&self.backend), id, info))
247    }
248
249    /// Receive the next event from the device.
250    ///
251    /// This method awaits **indefinitely** on the underlying event channel until an
252    /// event arrives or the device disconnects. Always wrap this in
253    /// `tokio::time::timeout` (or equivalent) so you can check for shutdown.
254    ///
255    /// # Concurrency
256    ///
257    /// On the USB backend, event reading uses the USB interrupt endpoint, which is
258    /// independent from the bulk endpoints used by file operations, so it is safe to
259    /// call `next_event()` concurrently with other `MtpDevice` methods.
260    ///
261    /// If you wrap `MtpDevice` in a shared lock (for example, `Arc<Mutex<MtpDevice>>`),
262    /// do **not** hold that lock while awaiting `next_event()`: it will block all file
263    /// operations for the duration of the wait. Instead, clone the `MtpDevice` (it is
264    /// cheaply cloneable via `Arc` internally) and call `next_event()` on the clone
265    /// without holding the lock.
266    ///
267    /// # Returns
268    ///
269    /// - `Ok(event)` - An event was received from the device
270    /// - `Err(Error::Disconnected)` - Device was disconnected
271    /// - `Err(_)` - Other communication error
272    ///
273    /// # Example
274    ///
275    /// ```rust,no_run
276    /// use mtp_rs::mtp::{MtpDevice, DeviceEvent};
277    /// use mtp_rs::Error;
278    /// use tokio::time::{timeout, Duration};
279    ///
280    /// # async fn example() -> Result<(), Error> {
281    /// # let device = MtpDevice::open_first().await?;
282    /// loop {
283    ///     match timeout(Duration::from_millis(200), device.next_event()).await {
284    ///         Ok(Ok(event)) => {
285    ///             match event {
286    ///                 DeviceEvent::ObjectAdded { handle } => {
287    ///                     println!("New object: {:?}", handle);
288    ///                 }
289    ///                 DeviceEvent::StoreRemoved { storage_id } => {
290    ///                     println!("Storage removed: {:?}", storage_id);
291    ///                 }
292    ///                 _ => {}
293    ///             }
294    ///         }
295    ///         Ok(Err(Error::Disconnected)) => break,
296    ///         Ok(Err(e)) => {
297    ///             eprintln!("Error: {}", e);
298    ///             break;
299    ///         }
300    ///         Err(_elapsed) => continue,  // Timeout, check for shutdown etc.
301    ///     }
302    /// }
303    /// # Ok(())
304    /// # }
305    /// ```
306    pub async fn next_event(&self) -> Result<DeviceEvent, Error> {
307        self.backend.next_event().await
308    }
309
310    /// Close the connection (best-effort; also happens on drop).
311    pub async fn close(self) -> Result<(), Error> {
312        self.backend.close().await
313    }
314}
315
316/// Information about an MTP device (without opening it).
317///
318/// This struct provides device identification at multiple levels:
319///
320/// - **Device identity** (`vendor_id`, `product_id`, `serial_number`): Identifies
321///   a specific physical device. Use this to recognize "John's phone" regardless
322///   of which USB port it's plugged into.
323///
324/// - **Port identity** (`location_id`): Identifies the physical USB port/location.
325///   Use this when you care about "the device on port 3" rather than which
326///   specific device it is. Stable across reconnections to the same port.
327///
328/// - **Display info** (`manufacturer`, `product`): Human-readable strings for
329///   showing device info to users.
330///
331/// # Example
332///
333/// ```rust,no_run
334/// use mtp_rs::mtp::MtpDevice;
335///
336/// let devices = MtpDevice::list_devices()?;
337/// for dev in &devices {
338///     println!("{} {} (serial: {:?})",
339///              dev.manufacturer.as_deref().unwrap_or("Unknown"),
340///              dev.product.as_deref().unwrap_or("Unknown"),
341///              dev.serial_number);
342/// }
343///
344/// // Save location_id to remember "the device on this port"
345/// // Save serial_number to remember "this specific phone"
346/// # Ok::<(), mtp_rs::Error>(())
347/// ```
348///
349/// Marked `#[non_exhaustive]` so future field additions don't break consumers
350/// that pattern-match or destructure. Construct via [`MtpDevice::list_devices`].
351#[derive(Debug, Clone)]
352#[non_exhaustive]
353pub struct MtpDeviceInfo {
354    /// USB vendor ID (assigned by USB-IF to each company).
355    ///
356    /// Examples: Google = `0x18d1`, Samsung = `0x04e8`, Apple = `0x05ac`
357    pub vendor_id: u16,
358
359    /// USB product ID (assigned by vendor to each product model).
360    ///
361    /// Note: The same device may report different product IDs depending on
362    /// its USB mode (MTP, ADB, charging-only, etc.).
363    pub product_id: u16,
364
365    /// Manufacturer name from USB descriptor.
366    ///
367    /// Examples: `"Google"`, `"Samsung"`, `"Apple Inc."`
368    ///
369    /// `None` if the device doesn't report a manufacturer string.
370    pub manufacturer: Option<String>,
371
372    /// Product name from USB descriptor.
373    ///
374    /// Examples: `"Pixel 9 Pro XL"`, `"Galaxy S24"`
375    ///
376    /// `None` if the device doesn't report a product string.
377    pub product: Option<String>,
378
379    /// Serial number uniquely identifying this specific device.
380    ///
381    /// Combined with `vendor_id` and `product_id`, this globally identifies
382    /// a single physical device. Survives reconnection to different ports.
383    ///
384    /// `None` if the device doesn't report a serial number.
385    pub serial_number: Option<String>,
386
387    /// Physical USB location identifier.
388    ///
389    /// Identifies the USB port/path where the device is connected. Stable
390    /// across reconnections to the same physical port, but changes if the
391    /// device is moved to a different port.
392    ///
393    /// Derived cross-platform from the USB bus ID and port chain (topology).
394    pub location_id: u64,
395
396    /// Negotiated USB link speed (slowest of host port, cable, and device).
397    ///
398    /// A USB 3.2 Gen 2 phone connected through a USB 2.0 charging cable
399    /// reports `High` (480 Mbit/s), not the device's capability.
400    ///
401    /// `None` if the OS doesn't report it for this device.
402    pub speed: Option<crate::transport::UsbSpeed>,
403
404    /// Why this USB device was classified as an MTP candidate.
405    pub match_reason: crate::transport::MtpMatchReason,
406}
407
408impl MtpDeviceInfo {
409    /// Build the neutral info from a USB-transport listing entry.
410    pub(crate) fn from_usb(d: crate::transport::UsbDeviceInfo) -> Self {
411        Self {
412            vendor_id: d.vendor_id,
413            product_id: d.product_id,
414            manufacturer: d.manufacturer,
415            product: d.product,
416            serial_number: d.serial_number,
417            location_id: d.location_id,
418            speed: d.speed,
419            match_reason: d.match_reason,
420        }
421    }
422
423    /// Format the device info for display.
424    #[must_use]
425    pub fn display(&self) -> String {
426        let manufacturer = self.manufacturer.as_deref().unwrap_or("Unknown");
427        let product = self.product.as_deref().unwrap_or("Unknown");
428        match &self.serial_number {
429            Some(serial) => format!(
430                "{} {} (serial: {}, location: {:08x})",
431                manufacturer, product, serial, self.location_id
432            ),
433            None => format!(
434                "{} {} (location: {:08x})",
435                manufacturer, product, self.location_id
436            ),
437        }
438    }
439}
440
441/// Builder for MtpDevice configuration.
442pub struct MtpDeviceBuilder {
443    timeout: Duration,
444    known_devices: Vec<(u16, u16)>,
445    backend: Backend,
446}
447
448impl MtpDeviceBuilder {
449    #[must_use]
450    pub fn new() -> Self {
451        Self {
452            timeout: NusbTransport::DEFAULT_TIMEOUT,
453            known_devices: Vec::new(),
454            backend: Backend::default(),
455        }
456    }
457
458    /// Choose which backend to open (default [`Backend::Auto`]).
459    ///
460    /// On Windows, `Auto` prefers WPD (for phones) and falls back to USB; pass [`Backend::Usb`] to
461    /// force PTP-over-USB to a Zadig/WinUSB-bound camera, or [`Backend::Wpd`] to force WPD.
462    #[must_use]
463    pub fn backend(mut self, backend: Backend) -> Self {
464        self.backend = backend;
465        self
466    }
467
468    /// If the configured backend selects WPD (Windows), try to open the first WPD device.
469    ///
470    /// Returns `Ok(None)` when WPD isn't selected, or when `Auto` found no WPD device (so the caller
471    /// falls back to USB). A forced [`Backend::Wpd`], or any non-"no device" WPD error, propagates.
472    async fn try_open_wpd_first(&self) -> Result<Option<MtpDevice>, Error> {
473        #[cfg(windows)]
474        if matches!(self.backend, Backend::Auto | Backend::Wpd) {
475            match crate::mtp::backend::wpd::WpdBackend::open_first().await {
476                Ok(b) => {
477                    return Ok(Some(MtpDevice {
478                        backend: Arc::new(b),
479                    }))
480                }
481                Err(e) if self.backend == Backend::Wpd || !matches!(e, Error::NoDevice) => {
482                    return Err(e)
483                }
484                Err(_) => {} // Auto + no WPD device: fall back to USB.
485            }
486        }
487        #[cfg(not(windows))]
488        if self.backend == Backend::Wpd {
489            return Err(Error::Unsupported);
490        }
491        Ok(None)
492    }
493
494    /// As [`try_open_wpd_first`](Self::try_open_wpd_first) but matching a serial number.
495    async fn try_open_wpd_by_serial(&self, serial: &str) -> Result<Option<MtpDevice>, Error> {
496        #[cfg(windows)]
497        if matches!(self.backend, Backend::Auto | Backend::Wpd) {
498            match crate::mtp::backend::wpd::WpdBackend::open_by_serial(serial).await {
499                Ok(b) => {
500                    return Ok(Some(MtpDevice {
501                        backend: Arc::new(b),
502                    }))
503                }
504                Err(e) if self.backend == Backend::Wpd || !matches!(e, Error::NoDevice) => {
505                    return Err(e)
506                }
507                Err(_) => {}
508            }
509        }
510        #[cfg(not(windows))]
511        {
512            let _ = serial;
513            if self.backend == Backend::Wpd {
514                return Err(Error::Unsupported);
515            }
516        }
517        Ok(None)
518    }
519
520    /// As [`try_open_wpd_by_serial`](Self::try_open_wpd_by_serial) but for a USB device (VID/PID plus
521    /// the USB descriptor serial), used to correlate an nusb `location_id` to a WPD device.
522    ///
523    /// The nusb and WPD *device* serials can differ (the Pixel's USB-descriptor serial isn't its WPD
524    /// serial), so the WPD side matches on VID/PID and, only when two identical models share it,
525    /// disambiguates by the USB serial resolved from the device tree.
526    async fn try_open_wpd_for_usb(
527        &self,
528        serial: Option<String>,
529        vid: u16,
530        pid: u16,
531    ) -> Result<Option<MtpDevice>, Error> {
532        #[cfg(windows)]
533        if matches!(self.backend, Backend::Auto | Backend::Wpd) {
534            match crate::mtp::backend::wpd::WpdBackend::open_for_usb(serial.clone(), vid, pid).await
535            {
536                Ok(b) => {
537                    return Ok(Some(MtpDevice {
538                        backend: Arc::new(b),
539                    }))
540                }
541                Err(e) if self.backend == Backend::Wpd || !matches!(e, Error::NoDevice) => {
542                    return Err(e)
543                }
544                Err(_) => {}
545            }
546        }
547        #[cfg(not(windows))]
548        {
549            let _ = (serial, vid, pid);
550            if self.backend == Backend::Wpd {
551                return Err(Error::Unsupported);
552            }
553        }
554        Ok(None)
555    }
556
557    /// Set bulk transfer timeout (default: 30 seconds).
558    ///
559    /// This timeout applies to file transfers, command responses, and event polling.
560    /// Use longer timeouts for large file operations.
561    #[must_use]
562    pub fn timeout(mut self, timeout: Duration) -> Self {
563        self.timeout = timeout;
564        self
565    }
566
567    /// Include additional devices identified by VID/PID pairs in the open-time
568    /// device scan.
569    ///
570    /// By default, the `open_first` / `open_by_serial` / `open_by_location`
571    /// convenience methods only consider devices whose USB descriptors match
572    /// standard MTP class codes. Pass extra VID/PID pairs here to also accept
573    /// legacy or otherwise unusual devices that speak MTP despite reporting
574    /// non-standard descriptors.
575    ///
576    /// This is the open-side counterpart to [`MtpDevice::list_devices_with_known`]:
577    /// pair them with the same list to enumerate and open the same set of devices.
578    #[must_use]
579    pub fn known_devices(mut self, known: &[(u16, u16)]) -> Self {
580        self.known_devices = known.to_vec();
581        self
582    }
583
584    /// Open the first available device.
585    pub async fn open_first(self) -> Result<MtpDevice, Error> {
586        if let Some(device) = self.try_open_wpd_first().await? {
587            return Ok(device);
588        }
589        let devices = NusbTransport::list_mtp_devices_with_known(&self.known_devices)?;
590        let device_info = devices
591            .into_iter()
592            .next()
593            .ok_or(crate::PtpError::NoDevice)?;
594        let device = device_info.open().map_err(crate::PtpError::Usb)?;
595        self.open_nusb_device(device).await
596    }
597
598    /// Open a device at a specific USB location (port).
599    ///
600    /// Use `MtpDevice::list_devices()` to get available location IDs.
601    /// Also checks the virtual device registry when the `virtual-device` feature is enabled.
602    ///
603    /// On Windows a phone at the location is bound to the WPD driver and can't be claimed over raw
604    /// USB, so this correlates the location to a WPD device and opens it there (for
605    /// [`Backend::Auto`]/[`Backend::Wpd`]); it falls back to raw USB for WinUSB-bound cameras and on
606    /// other platforms. The correlation is by **VID/PID** (the USB-descriptor serial and the WPD
607    /// serial can differ), so with two attached devices of the *same model* it may open the other
608    /// one — address those by serial instead.
609    pub async fn open_by_location(self, location_id: u64) -> Result<MtpDevice, Error> {
610        #[cfg(feature = "virtual-device")]
611        if let Some(config) =
612            crate::transport::virtual_device::registry::find_virtual_config_by_location(location_id)
613        {
614            return self.open_virtual(config).await;
615        }
616
617        let devices = NusbTransport::list_mtp_devices_with_known(&self.known_devices)?;
618        let device_info = devices
619            .into_iter()
620            .find(|d| d.location_id == location_id)
621            .ok_or(crate::PtpError::NoDevice)?;
622
623        if let Some(device) = self
624            .try_open_wpd_for_usb(
625                device_info.serial_number.clone(),
626                device_info.vendor_id,
627                device_info.product_id,
628            )
629            .await?
630        {
631            return Ok(device);
632        }
633
634        let device = device_info.open().map_err(crate::PtpError::Usb)?;
635        self.open_nusb_device(device).await
636    }
637
638    /// Open a device by its serial number.
639    ///
640    /// This identifies a specific physical device regardless of which USB port
641    /// it's connected to. Also checks the virtual device registry when the
642    /// `virtual-device` feature is enabled.
643    pub async fn open_by_serial(self, serial: &str) -> Result<MtpDevice, Error> {
644        #[cfg(feature = "virtual-device")]
645        if let Some(config) =
646            crate::transport::virtual_device::registry::find_virtual_config_by_serial(serial)
647        {
648            return self.open_virtual(config).await;
649        }
650
651        if let Some(device) = self.try_open_wpd_by_serial(serial).await? {
652            return Ok(device);
653        }
654
655        let devices = NusbTransport::list_mtp_devices_with_known(&self.known_devices)?;
656        let device_info = devices
657            .into_iter()
658            .find(|d| d.serial_number.as_deref() == Some(serial))
659            .ok_or(crate::PtpError::NoDevice)?;
660        let device = device_info.open().map_err(crate::PtpError::Usb)?;
661        self.open_nusb_device(device).await
662    }
663
664    /// Open an already-acquired [`nusb::Device`] as an MTP device.
665    ///
666    /// This is an escape hatch for consumers who already hold an `nusb::Device`
667    /// (e.g. from a custom enumeration or hotplug watcher). For most callers,
668    /// prefer [`known_devices`](Self::known_devices) combined with
669    /// `open_by_serial` / `open_by_location`.
670    ///
671    /// The interface scan is permissive: strict MTP-class match first, then
672    /// fallback to any interface with the MTP endpoint layout.
673    ///
674    /// # Example
675    ///
676    /// ```rust,no_run
677    /// use mtp_rs::mtp::MtpDevice;
678    /// use nusb::MaybeFuture;
679    ///
680    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
681    /// let nusb_device = nusb::list_devices()
682    ///     .wait()?
683    ///     .find(|d: &nusb::DeviceInfo| {
684    ///         d.vendor_id() == 0x045E && d.product_id() == 0x0710
685    ///     })
686    ///     .ok_or(mtp_rs::Error::NoDevice)?
687    ///     .open()
688    ///     .wait()?;
689    ///
690    /// let device = MtpDevice::builder()
691    ///     .open_nusb_device(nusb_device)
692    ///     .await?;
693    /// # Ok(())
694    /// # }
695    /// ```
696    pub async fn open_nusb_device(self, device: nusb::Device) -> Result<MtpDevice, Error> {
697        let transport = NusbTransport::open_with_timeout(device, self.timeout).await?;
698        let transport: Arc<dyn Transport> = Arc::new(transport);
699
700        // Open session (use session ID 1)
701        let session = Arc::new(PtpSession::open(transport.clone(), 1).await?);
702
703        // Get device info
704        let device_info = session.get_device_info().await?;
705
706        // Quirk for Garmin devices
707        if device_info.manufacturer == "Garmin" {
708            session.set_split_header_data(true);
709        }
710
711        let backend = UsbBackend::new(session, device_info);
712        Ok(MtpDevice {
713            backend: Arc::new(backend),
714        })
715    }
716
717    /// Reset the USB transport of the device with this serial, without opening a
718    /// session. See [`MtpDevice::reset_by_serial`] for the full contract.
719    ///
720    /// **Last resort on Android**: the reset can break the phone's MTP function
721    /// until the user physically replugs. Try spaced reopens first.
722    pub async fn reset_by_serial(self, serial: &str) -> Result<(), Error> {
723        #[cfg(feature = "virtual-device")]
724        if crate::transport::virtual_device::registry::find_virtual_config_by_serial(serial)
725            .is_some()
726        {
727            return Err(Error::Unsupported);
728        }
729
730        let devices = NusbTransport::list_mtp_devices_with_known(&self.known_devices)?;
731        let device_info = devices
732            .into_iter()
733            .find(|d| d.serial_number.as_deref() == Some(serial))
734            .ok_or(crate::PtpError::NoDevice)?;
735        self.reset_usb_device(device_info).await
736    }
737
738    /// Reset the USB transport of the device at this location, without opening a
739    /// session. See [`MtpDevice::reset_by_serial`] for the full contract.
740    ///
741    /// **Last resort on Android**: the reset can break the phone's MTP function
742    /// until the user physically replugs. Try spaced reopens first.
743    pub async fn reset_by_location(self, location_id: u64) -> Result<(), Error> {
744        #[cfg(feature = "virtual-device")]
745        if crate::transport::virtual_device::registry::find_virtual_config_by_location(location_id)
746            .is_some()
747        {
748            return Err(Error::Unsupported);
749        }
750
751        let devices = NusbTransport::list_mtp_devices_with_known(&self.known_devices)?;
752        let device_info = devices
753            .into_iter()
754            .find(|d| d.location_id == location_id)
755            .ok_or(crate::PtpError::NoDevice)?;
756        self.reset_usb_device(device_info).await
757    }
758
759    /// Reset the USB transport of the first available device, without opening a
760    /// session. See [`MtpDevice::reset_by_serial`] for the full contract.
761    ///
762    /// **Last resort on Android**: the reset can break the phone's MTP function
763    /// until the user physically replugs. Try spaced reopens first.
764    pub async fn reset_first(self) -> Result<(), Error> {
765        let devices = NusbTransport::list_mtp_devices_with_known(&self.known_devices)?;
766        let device_info = devices
767            .into_iter()
768            .next()
769            .ok_or(crate::PtpError::NoDevice)?;
770        self.reset_usb_device(device_info).await
771    }
772
773    /// Claim the interface and send the transport reset. Deliberately does NOT
774    /// call [`PtpSession::open`] or `GetDeviceInfo`: claiming is all a wedged
775    /// device can still answer.
776    async fn reset_usb_device(
777        self,
778        device_info: crate::transport::UsbDeviceInfo,
779    ) -> Result<(), Error> {
780        let device = device_info.open().map_err(crate::PtpError::Usb)?;
781        let transport = NusbTransport::open_with_timeout(device, self.timeout).await?;
782        transport.reset_device().await?;
783        Ok(())
784    }
785
786    /// Open a virtual device backed by local filesystem directories.
787    ///
788    /// This creates a virtual MTP device that speaks the full binary protocol but
789    /// operates against local directories instead of USB. Use this for testing MTP
790    /// client code without real hardware.
791    ///
792    /// # Example
793    ///
794    /// ```rust,no_run
795    /// use std::path::PathBuf;
796    /// use mtp_rs::MtpDevice;
797    /// use mtp_rs::transport::virtual_device::config::{VirtualDeviceConfig, VirtualStorageConfig};
798    ///
799    /// # async fn example() -> Result<(), mtp_rs::Error> {
800    /// let device = MtpDevice::builder()
801    ///     .open_virtual(VirtualDeviceConfig {
802    ///         manufacturer: "Google".into(),
803    ///         model: "Virtual Pixel 9".into(),
804    ///         serial: "virtual-001".into(),
805    ///         storages: vec![VirtualStorageConfig {
806    ///             description: "Internal Storage".into(),
807    ///             capacity: 64 * 1024 * 1024 * 1024,
808    ///             backing_dir: PathBuf::from("/tmp/mtp-test"),
809    ///             read_only: false,
810    ///         }],
811    ///         ..Default::default()
812    ///     })
813    ///     .await?;
814    /// # Ok(())
815    /// # }
816    /// ```
817    #[cfg(feature = "virtual-device")]
818    pub async fn open_virtual(
819        self,
820        config: crate::transport::virtual_device::config::VirtualDeviceConfig,
821    ) -> Result<MtpDevice, Error> {
822        if config.storages.is_empty() {
823            return Err(Error::invalid_data(
824                "VirtualDeviceConfig requires at least one storage",
825            ));
826        }
827
828        let transport = crate::transport::virtual_device::VirtualTransport::new(config);
829        let transport: Arc<dyn Transport> = Arc::new(transport);
830
831        // Open session (use session ID 1)
832        let session = Arc::new(PtpSession::open(transport.clone(), 1).await?);
833
834        // Get device info
835        let device_info = session.get_device_info().await?;
836
837        let backend = UsbBackend::new(session, device_info);
838        Ok(MtpDevice {
839            backend: Arc::new(backend),
840        })
841    }
842}
843
844impl Default for MtpDeviceBuilder {
845    fn default() -> Self {
846        Self::new()
847    }
848}
849
850#[cfg(test)]
851mod tests {
852    use super::*;
853
854    #[test]
855    fn list_devices_returns_ok() {
856        assert!(MtpDevice::list_devices().is_ok());
857    }
858
859    #[tokio::test]
860    async fn resetting_an_absent_device_reports_no_device() {
861        let err = MtpDevice::reset_by_serial("no-such-device-serial")
862            .await
863            .expect_err("no USB device has that serial");
864        assert!(matches!(err, Error::NoDevice), "got {err:?}");
865    }
866
867    #[cfg(feature = "virtual-device")]
868    #[tokio::test]
869    async fn resetting_a_virtual_device_says_it_has_no_transport_to_reset() {
870        let dir = tempfile::tempdir().unwrap();
871        let serial = "reset-virtual-serial";
872        let config = crate::VirtualDeviceConfig {
873            serial: serial.into(),
874            storages: vec![crate::VirtualStorageConfig {
875                description: "Internal Storage".into(),
876                capacity: 1024,
877                backing_dir: dir.path().to_path_buf(),
878                read_only: false,
879            }],
880            ..Default::default()
881        };
882        let info = crate::register_virtual_device(&config);
883
884        // A virtual device is a filesystem, not a USB link: there's no transport
885        // state to reset. Saying so beats a puzzling "no device found" in a
886        // consumer's test suite, which is where this will actually be hit.
887        let err = MtpDevice::reset_by_serial(serial)
888            .await
889            .expect_err("a virtual device has no USB transport");
890        assert!(matches!(err, Error::Unsupported), "got {err:?}");
891
892        crate::unregister_virtual_device(info.location_id);
893    }
894
895    #[test]
896    fn builder_timeout() {
897        // Default value
898        let builder = MtpDeviceBuilder::new();
899        assert_eq!(builder.timeout, NusbTransport::DEFAULT_TIMEOUT);
900
901        // Custom value
902        let custom = MtpDeviceBuilder::new().timeout(Duration::from_secs(45));
903        assert_eq!(custom.timeout, Duration::from_secs(45));
904    }
905
906    #[test]
907    fn device_info_display() {
908        let with_serial = MtpDeviceInfo {
909            vendor_id: 0x04e8,
910            product_id: 0x6860,
911            manufacturer: Some("Samsung".to_string()),
912            product: Some("Galaxy S24".to_string()),
913            serial_number: Some("ABC123".to_string()),
914            location_id: 0x00200000,
915            speed: None,
916            match_reason: crate::transport::MtpMatchReason::StandardClass,
917        };
918        let display = with_serial.display();
919        assert!(display.contains("Samsung") && display.contains("Galaxy S24"));
920        assert!(display.contains("ABC123") && display.contains("00200000"));
921
922        // Without serial
923        let no_serial = MtpDeviceInfo {
924            serial_number: None,
925            ..with_serial.clone()
926        };
927        assert!(!no_serial.display().contains("serial:"));
928
929        // Unknown manufacturer
930        let unknown = MtpDeviceInfo {
931            manufacturer: None,
932            product: None,
933            ..with_serial
934        };
935        assert!(unknown.display().contains("Unknown"));
936    }
937
938    #[cfg(feature = "virtual-device")]
939    #[tokio::test]
940    async fn open_virtual_empty_storages_rejected() {
941        use crate::transport::virtual_device::config::VirtualDeviceConfig;
942
943        let config = VirtualDeviceConfig {
944            serial: "empty-001".into(),
945            // The point of this test: an empty `storages` must be rejected.
946            storages: vec![],
947            ..Default::default()
948        };
949
950        let result = MtpDevice::builder().open_virtual(config).await;
951        match result {
952            Err(err) => assert!(
953                err.to_string().contains("at least one storage"),
954                "unexpected error: {}",
955                err
956            ),
957            Ok(_) => panic!("expected error for empty storages"),
958        }
959    }
960
961    #[tokio::test]
962    #[ignore] // Requires real MTP device
963    async fn real_device_operations() {
964        let device = MtpDevice::open_first().await.unwrap();
965        println!("Connected to: {}", device.device_info().model);
966        for storage in device.storages().await.unwrap() {
967            println!("Storage: {}", storage.info().description);
968        }
969        device.close().await.unwrap();
970    }
971}