Skip to main content

mtp_rs/mtp/
device.rs

1//! MtpDevice - the main entry point for MTP operations.
2
3use crate::mtp::{DeviceEvent, Storage};
4use crate::ptp::{DeviceInfo, ObjectHandle, PtpSession, StorageId};
5use crate::transport::{NusbTransport, Transport};
6use crate::Error;
7use std::sync::Arc;
8use std::time::Duration;
9
10/// Internal shared state for MtpDevice.
11pub(crate) struct MtpDeviceInner {
12    pub(crate) session: Arc<PtpSession>,
13    pub(crate) device_info: DeviceInfo,
14}
15
16impl MtpDeviceInner {
17    /// Check if the device is an Android device.
18    ///
19    /// Detected by looking for "android.com" in the vendor extension descriptor.
20    /// Android devices have known MTP quirks (e.g., ObjectHandle::ALL doesn't work
21    /// for recursive listing).
22    #[must_use]
23    pub fn is_android(&self) -> bool {
24        self.device_info
25            .vendor_extension_desc
26            .to_lowercase()
27            .contains("android.com")
28    }
29}
30
31/// An MTP device connection.
32///
33/// This is the main entry point for interacting with MTP devices.
34/// Use `MtpDevice::open_first()` to connect to the first available device,
35/// or `MtpDevice::builder()` for more control.
36///
37/// # Example
38///
39/// ```rust,ignore
40/// use mtp_rs::mtp::MtpDevice;
41///
42/// # async fn example() -> Result<(), mtp_rs::Error> {
43/// // Open the first MTP device
44/// let device = MtpDevice::open_first().await?;
45///
46/// println!("Connected to: {} {}",
47///          device.device_info().manufacturer,
48///          device.device_info().model);
49///
50/// // Get storages
51/// for storage in device.storages().await? {
52///     println!("Storage: {} ({} free)",
53///              storage.info().description,
54///              storage.info().free_space_bytes);
55/// }
56/// # Ok(())
57/// # }
58/// ```
59pub struct MtpDevice {
60    inner: Arc<MtpDeviceInner>,
61}
62
63impl MtpDevice {
64    /// Create a builder for configuring device options.
65    pub fn builder() -> MtpDeviceBuilder {
66        MtpDeviceBuilder::new()
67    }
68
69    /// Open the first available MTP device with default settings.
70    pub async fn open_first() -> Result<Self, Error> {
71        Self::builder().open_first().await
72    }
73
74    /// Open a device at a specific USB location (port) with default settings.
75    ///
76    /// Use `list_devices()` to get available location IDs.
77    pub async fn open_by_location(location_id: u64) -> Result<Self, Error> {
78        Self::builder().open_by_location(location_id).await
79    }
80
81    /// Open a device by its serial number with default settings.
82    ///
83    /// This identifies a specific physical device regardless of which USB port
84    /// it's connected to.
85    pub async fn open_by_serial(serial: &str) -> Result<Self, Error> {
86        Self::builder().open_by_serial(serial).await
87    }
88
89    /// List all available MTP devices without opening them.
90    pub fn list_devices() -> Result<Vec<MtpDeviceInfo>, Error> {
91        let devices = NusbTransport::list_mtp_devices()?;
92        Ok(devices
93            .into_iter()
94            .map(|d| MtpDeviceInfo {
95                vendor_id: d.vendor_id,
96                product_id: d.product_id,
97                manufacturer: d.manufacturer,
98                product: d.product,
99                serial_number: d.serial_number,
100                location_id: d.location_id,
101            })
102            .collect())
103    }
104
105    /// Get device information.
106    #[must_use]
107    pub fn device_info(&self) -> &DeviceInfo {
108        &self.inner.device_info
109    }
110
111    /// Check if the device supports renaming objects.
112    ///
113    /// This checks for support of the SetObjectPropValue operation (0x9804),
114    /// which is required to rename files and folders via the ObjectFileName property.
115    ///
116    /// # Returns
117    ///
118    /// Returns true if the device advertises SetObjectPropValue support.
119    #[must_use]
120    pub fn supports_rename(&self) -> bool {
121        self.inner.device_info.supports_rename()
122    }
123
124    /// Get all storages on the device.
125    pub async fn storages(&self) -> Result<Vec<Storage>, Error> {
126        let ids = self.inner.session.get_storage_ids().await?;
127        let mut storages = Vec::with_capacity(ids.len());
128        for id in ids {
129            let info = self.inner.session.get_storage_info(id).await?;
130            storages.push(Storage::new(self.inner.clone(), id, info));
131        }
132        Ok(storages)
133    }
134
135    /// Get a specific storage by ID.
136    pub async fn storage(&self, id: StorageId) -> Result<Storage, Error> {
137        let info = self.inner.session.get_storage_info(id).await?;
138        Ok(Storage::new(self.inner.clone(), id, info))
139    }
140
141    /// Get object handles in a storage.
142    ///
143    /// # Arguments
144    ///
145    /// * `storage_id` - Storage to search, or `StorageId::ALL` for all storages
146    /// * `parent` - Parent folder handle, or `None` for root level only,
147    ///   or `Some(ObjectHandle::ALL)` for recursive listing
148    pub async fn get_object_handles(
149        &self,
150        storage_id: StorageId,
151        parent: Option<ObjectHandle>,
152    ) -> Result<Vec<ObjectHandle>, Error> {
153        self.inner
154            .session
155            .get_object_handles(storage_id, None, parent)
156            .await
157    }
158
159    /// Receive the next event from the device.
160    ///
161    /// This method waits for an event on the USB interrupt endpoint, returning when
162    /// an event arrives or the event timeout expires (default: 200ms). The short
163    /// default timeout allows responsive event loops without blocking other operations.
164    ///
165    /// Configure the timeout via [`MtpDeviceBuilder::event_timeout()`].
166    ///
167    /// # Returns
168    ///
169    /// - `Ok(event)` - An event was received from the device
170    /// - `Err(Error::Timeout)` - No event within the timeout period (normal, keep polling)
171    /// - `Err(Error::Disconnected)` - Device was disconnected
172    /// - `Err(_)` - Other communication error
173    ///
174    /// # Example
175    ///
176    /// ```rust,ignore
177    /// // Configure event timeout if needed (default is 200ms)
178    /// let device = MtpDevice::builder()
179    ///     .event_timeout(Duration::from_millis(100))
180    ///     .open_first()
181    ///     .await?;
182    ///
183    /// loop {
184    ///     match device.next_event().await {
185    ///         Ok(event) => {
186    ///             match event {
187    ///                 DeviceEvent::ObjectAdded { handle } => {
188    ///                     println!("New object: {:?}", handle);
189    ///                 }
190    ///                 DeviceEvent::StoreRemoved { storage_id } => {
191    ///                     println!("Storage removed: {:?}", storage_id);
192    ///                 }
193    ///                 _ => {}
194    ///             }
195    ///         }
196    ///         Err(Error::Disconnected) => break,
197    ///         Err(Error::Timeout) => continue,  // No event, keep polling
198    ///         Err(e) => {
199    ///             eprintln!("Error: {}", e);
200    ///             break;
201    ///         }
202    ///     }
203    /// }
204    /// ```
205    pub async fn next_event(&self) -> Result<DeviceEvent, Error> {
206        match self.inner.session.poll_event().await? {
207            Some(container) => Ok(DeviceEvent::from_container(&container)),
208            None => Err(Error::Timeout),
209        }
210    }
211
212    /// Close the connection (also happens on drop).
213    pub async fn close(self) -> Result<(), Error> {
214        // Try to close gracefully, but Arc might have multiple references
215        if let Ok(inner) = Arc::try_unwrap(self.inner) {
216            if let Ok(session) = Arc::try_unwrap(inner.session) {
217                session.close().await?;
218            }
219        }
220        Ok(())
221    }
222}
223
224/// Information about an MTP device (without opening it).
225///
226/// This struct provides device identification at multiple levels:
227///
228/// - **Device identity** (`vendor_id`, `product_id`, `serial_number`): Identifies
229///   a specific physical device. Use this to recognize "John's phone" regardless
230///   of which USB port it's plugged into.
231///
232/// - **Port identity** (`location_id`): Identifies the physical USB port/location.
233///   Use this when you care about "the device on port 3" rather than which
234///   specific device it is. Stable across reconnections to the same port.
235///
236/// - **Display info** (`manufacturer`, `product`): Human-readable strings for
237///   showing device info to users.
238///
239/// # Example
240///
241/// ```rust,ignore
242/// let devices = MtpDevice::list_devices()?;
243/// for dev in &devices {
244///     println!("{} {} (serial: {:?})",
245///              dev.manufacturer.as_deref().unwrap_or("Unknown"),
246///              dev.product.as_deref().unwrap_or("Unknown"),
247///              dev.serial_number);
248/// }
249///
250/// // Save location_id to remember "the device on this port"
251/// // Save serial_number to remember "this specific phone"
252/// ```
253#[derive(Debug, Clone)]
254pub struct MtpDeviceInfo {
255    /// USB vendor ID (assigned by USB-IF to each company).
256    ///
257    /// Examples: Google = `0x18d1`, Samsung = `0x04e8`, Apple = `0x05ac`
258    pub vendor_id: u16,
259
260    /// USB product ID (assigned by vendor to each product model).
261    ///
262    /// Note: The same device may report different product IDs depending on
263    /// its USB mode (MTP, ADB, charging-only, etc.).
264    pub product_id: u16,
265
266    /// Manufacturer name from USB descriptor.
267    ///
268    /// Examples: `"Google"`, `"Samsung"`, `"Apple Inc."`
269    ///
270    /// `None` if the device doesn't report a manufacturer string.
271    pub manufacturer: Option<String>,
272
273    /// Product name from USB descriptor.
274    ///
275    /// Examples: `"Pixel 9 Pro XL"`, `"Galaxy S24"`
276    ///
277    /// `None` if the device doesn't report a product string.
278    pub product: Option<String>,
279
280    /// Serial number uniquely identifying this specific device.
281    ///
282    /// Combined with `vendor_id` and `product_id`, this globally identifies
283    /// a single physical device. Survives reconnection to different ports.
284    ///
285    /// `None` if the device doesn't report a serial number.
286    pub serial_number: Option<String>,
287
288    /// Physical USB location identifier.
289    ///
290    /// Identifies the USB port/path where the device is connected. Stable
291    /// across reconnections to the same physical port, but changes if the
292    /// device is moved to a different port.
293    ///
294    /// Platform details:
295    /// - **macOS**: IOKit `locationID` encoding the port path
296    /// - **Linux**: Derived from sysfs bus/port path
297    /// - **Windows**: `LocationInformation` property
298    pub location_id: u64,
299}
300
301impl MtpDeviceInfo {
302    /// Format the device info for display.
303    #[must_use]
304    pub fn display(&self) -> String {
305        let manufacturer = self.manufacturer.as_deref().unwrap_or("Unknown");
306        let product = self.product.as_deref().unwrap_or("Unknown");
307        match &self.serial_number {
308            Some(serial) => format!(
309                "{} {} (serial: {}, location: {:08x})",
310                manufacturer, product, serial, self.location_id
311            ),
312            None => format!(
313                "{} {} (location: {:08x})",
314                manufacturer, product, self.location_id
315            ),
316        }
317    }
318}
319
320/// Builder for MtpDevice configuration.
321pub struct MtpDeviceBuilder {
322    timeout: Duration,
323    event_timeout: Duration,
324}
325
326impl MtpDeviceBuilder {
327    #[must_use]
328    pub fn new() -> Self {
329        Self {
330            timeout: NusbTransport::DEFAULT_TIMEOUT,
331            event_timeout: NusbTransport::DEFAULT_EVENT_TIMEOUT,
332        }
333    }
334
335    /// Set bulk transfer timeout (default: 30 seconds).
336    ///
337    /// This timeout applies to file transfers and command responses.
338    /// Use longer timeouts for large file operations.
339    #[must_use]
340    pub fn timeout(mut self, timeout: Duration) -> Self {
341        self.timeout = timeout;
342        self
343    }
344
345    /// Set event polling timeout (default: 200ms).
346    ///
347    /// This timeout controls how long `next_event()` waits for device events.
348    /// Shorter timeouts (100-500ms) enable responsive event loops without
349    /// blocking other operations. Longer timeouts reduce polling overhead.
350    #[must_use]
351    pub fn event_timeout(mut self, timeout: Duration) -> Self {
352        self.event_timeout = timeout;
353        self
354    }
355
356    /// Open the first available device.
357    pub async fn open_first(self) -> Result<MtpDevice, Error> {
358        let devices = NusbTransport::list_mtp_devices()?;
359        let device_info = devices.into_iter().next().ok_or(Error::NoDevice)?;
360        let device = device_info.open().map_err(Error::Usb)?;
361        self.open_device(device).await
362    }
363
364    /// Open a device at a specific USB location (port).
365    ///
366    /// Use `MtpDevice::list_devices()` to get available location IDs.
367    pub async fn open_by_location(self, location_id: u64) -> Result<MtpDevice, Error> {
368        let devices = NusbTransport::list_mtp_devices()?;
369        let device_info = devices
370            .into_iter()
371            .find(|d| d.location_id == location_id)
372            .ok_or(Error::NoDevice)?;
373        let device = device_info.open().map_err(Error::Usb)?;
374        self.open_device(device).await
375    }
376
377    /// Open a device by its serial number.
378    ///
379    /// This identifies a specific physical device regardless of which USB port
380    /// it's connected to.
381    pub async fn open_by_serial(self, serial: &str) -> Result<MtpDevice, Error> {
382        let devices = NusbTransport::list_mtp_devices()?;
383        let device_info = devices
384            .into_iter()
385            .find(|d| d.serial_number.as_deref() == Some(serial))
386            .ok_or(Error::NoDevice)?;
387        let device = device_info.open().map_err(Error::Usb)?;
388        self.open_device(device).await
389    }
390
391    /// Internal: open an already-discovered device.
392    async fn open_device(self, device: nusb::Device) -> Result<MtpDevice, Error> {
393        // Open transport
394        let transport =
395            NusbTransport::open_with_timeouts(device, self.timeout, self.event_timeout).await?;
396        let transport: Arc<dyn Transport> = Arc::new(transport);
397
398        // Open session (use session ID 1)
399        let session = Arc::new(PtpSession::open(transport.clone(), 1).await?);
400
401        // Get device info
402        let device_info = session.get_device_info().await?;
403
404        let inner = Arc::new(MtpDeviceInner {
405            session,
406            device_info,
407        });
408
409        Ok(MtpDevice { inner })
410    }
411}
412
413impl Default for MtpDeviceBuilder {
414    fn default() -> Self {
415        Self::new()
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    #[test]
424    fn list_devices_returns_ok() {
425        assert!(MtpDevice::list_devices().is_ok());
426    }
427
428    #[test]
429    fn builder_timeouts() {
430        // Default values
431        let builder = MtpDeviceBuilder::new();
432        assert_eq!(builder.timeout, NusbTransport::DEFAULT_TIMEOUT);
433        assert_eq!(builder.event_timeout, NusbTransport::DEFAULT_EVENT_TIMEOUT);
434
435        // Custom values
436        let custom = MtpDeviceBuilder::new()
437            .timeout(Duration::from_secs(45))
438            .event_timeout(Duration::from_millis(100));
439        assert_eq!(custom.timeout, Duration::from_secs(45));
440        assert_eq!(custom.event_timeout, Duration::from_millis(100));
441    }
442
443    #[test]
444    fn device_info_display() {
445        let with_serial = MtpDeviceInfo {
446            vendor_id: 0x04e8,
447            product_id: 0x6860,
448            manufacturer: Some("Samsung".to_string()),
449            product: Some("Galaxy S24".to_string()),
450            serial_number: Some("ABC123".to_string()),
451            location_id: 0x00200000,
452        };
453        let display = with_serial.display();
454        assert!(display.contains("Samsung") && display.contains("Galaxy S24"));
455        assert!(display.contains("ABC123") && display.contains("00200000"));
456
457        // Without serial
458        let no_serial = MtpDeviceInfo {
459            serial_number: None,
460            ..with_serial.clone()
461        };
462        assert!(!no_serial.display().contains("serial:"));
463
464        // Unknown manufacturer
465        let unknown = MtpDeviceInfo {
466            manufacturer: None,
467            product: None,
468            ..with_serial
469        };
470        assert!(unknown.display().contains("Unknown"));
471    }
472
473    #[tokio::test]
474    #[ignore] // Requires real MTP device
475    async fn real_device_operations() {
476        let device = MtpDevice::open_first().await.unwrap();
477        println!("Connected to: {}", device.device_info().model);
478        for storage in device.storages().await.unwrap() {
479            println!("Storage: {}", storage.info().description);
480        }
481        device.close().await.unwrap();
482    }
483}