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. It will block
162    /// (up to the bulk transfer timeout) until an event arrives. Callers should use
163    /// their own async cancellation (e.g., `tokio::select!` or `tokio::time::timeout`)
164    /// for event loop control.
165    ///
166    /// # Returns
167    ///
168    /// - `Ok(event)` - An event was received from the device
169    /// - `Err(Error::Timeout)` - No event within the timeout period
170    /// - `Err(Error::Disconnected)` - Device was disconnected
171    /// - `Err(_)` - Other communication error
172    ///
173    /// # Example
174    ///
175    /// ```rust,ignore
176    /// use tokio::time::{timeout, Duration};
177    ///
178    /// loop {
179    ///     match timeout(Duration::from_millis(200), device.next_event()).await {
180    ///         Ok(Ok(event)) => {
181    ///             match event {
182    ///                 DeviceEvent::ObjectAdded { handle } => {
183    ///                     println!("New object: {:?}", handle);
184    ///                 }
185    ///                 DeviceEvent::StoreRemoved { storage_id } => {
186    ///                     println!("Storage removed: {:?}", storage_id);
187    ///                 }
188    ///                 _ => {}
189    ///             }
190    ///         }
191    ///         Ok(Err(Error::Disconnected)) => break,
192    ///         Ok(Err(e)) => {
193    ///             eprintln!("Error: {}", e);
194    ///             break;
195    ///         }
196    ///         Err(_elapsed) => continue,  // Timeout, check for shutdown etc.
197    ///     }
198    /// }
199    /// ```
200    pub async fn next_event(&self) -> Result<DeviceEvent, Error> {
201        match self.inner.session.poll_event().await? {
202            Some(container) => Ok(DeviceEvent::from_container(&container)),
203            None => Err(Error::Timeout),
204        }
205    }
206
207    /// Close the connection (also happens on drop).
208    pub async fn close(self) -> Result<(), Error> {
209        // Try to close gracefully, but Arc might have multiple references
210        if let Ok(inner) = Arc::try_unwrap(self.inner) {
211            if let Ok(session) = Arc::try_unwrap(inner.session) {
212                session.close().await?;
213            }
214        }
215        Ok(())
216    }
217}
218
219/// Information about an MTP device (without opening it).
220///
221/// This struct provides device identification at multiple levels:
222///
223/// - **Device identity** (`vendor_id`, `product_id`, `serial_number`): Identifies
224///   a specific physical device. Use this to recognize "John's phone" regardless
225///   of which USB port it's plugged into.
226///
227/// - **Port identity** (`location_id`): Identifies the physical USB port/location.
228///   Use this when you care about "the device on port 3" rather than which
229///   specific device it is. Stable across reconnections to the same port.
230///
231/// - **Display info** (`manufacturer`, `product`): Human-readable strings for
232///   showing device info to users.
233///
234/// # Example
235///
236/// ```rust,ignore
237/// let devices = MtpDevice::list_devices()?;
238/// for dev in &devices {
239///     println!("{} {} (serial: {:?})",
240///              dev.manufacturer.as_deref().unwrap_or("Unknown"),
241///              dev.product.as_deref().unwrap_or("Unknown"),
242///              dev.serial_number);
243/// }
244///
245/// // Save location_id to remember "the device on this port"
246/// // Save serial_number to remember "this specific phone"
247/// ```
248#[derive(Debug, Clone)]
249pub struct MtpDeviceInfo {
250    /// USB vendor ID (assigned by USB-IF to each company).
251    ///
252    /// Examples: Google = `0x18d1`, Samsung = `0x04e8`, Apple = `0x05ac`
253    pub vendor_id: u16,
254
255    /// USB product ID (assigned by vendor to each product model).
256    ///
257    /// Note: The same device may report different product IDs depending on
258    /// its USB mode (MTP, ADB, charging-only, etc.).
259    pub product_id: u16,
260
261    /// Manufacturer name from USB descriptor.
262    ///
263    /// Examples: `"Google"`, `"Samsung"`, `"Apple Inc."`
264    ///
265    /// `None` if the device doesn't report a manufacturer string.
266    pub manufacturer: Option<String>,
267
268    /// Product name from USB descriptor.
269    ///
270    /// Examples: `"Pixel 9 Pro XL"`, `"Galaxy S24"`
271    ///
272    /// `None` if the device doesn't report a product string.
273    pub product: Option<String>,
274
275    /// Serial number uniquely identifying this specific device.
276    ///
277    /// Combined with `vendor_id` and `product_id`, this globally identifies
278    /// a single physical device. Survives reconnection to different ports.
279    ///
280    /// `None` if the device doesn't report a serial number.
281    pub serial_number: Option<String>,
282
283    /// Physical USB location identifier.
284    ///
285    /// Identifies the USB port/path where the device is connected. Stable
286    /// across reconnections to the same physical port, but changes if the
287    /// device is moved to a different port.
288    ///
289    /// Derived cross-platform from the USB bus ID and port chain (topology).
290    pub location_id: u64,
291}
292
293impl MtpDeviceInfo {
294    /// Format the device info for display.
295    #[must_use]
296    pub fn display(&self) -> String {
297        let manufacturer = self.manufacturer.as_deref().unwrap_or("Unknown");
298        let product = self.product.as_deref().unwrap_or("Unknown");
299        match &self.serial_number {
300            Some(serial) => format!(
301                "{} {} (serial: {}, location: {:08x})",
302                manufacturer, product, serial, self.location_id
303            ),
304            None => format!(
305                "{} {} (location: {:08x})",
306                manufacturer, product, self.location_id
307            ),
308        }
309    }
310}
311
312/// Builder for MtpDevice configuration.
313pub struct MtpDeviceBuilder {
314    timeout: Duration,
315}
316
317impl MtpDeviceBuilder {
318    #[must_use]
319    pub fn new() -> Self {
320        Self {
321            timeout: NusbTransport::DEFAULT_TIMEOUT,
322        }
323    }
324
325    /// Set bulk transfer timeout (default: 30 seconds).
326    ///
327    /// This timeout applies to file transfers, command responses, and event polling.
328    /// Use longer timeouts for large file operations.
329    #[must_use]
330    pub fn timeout(mut self, timeout: Duration) -> Self {
331        self.timeout = timeout;
332        self
333    }
334
335    /// Open the first available device.
336    pub async fn open_first(self) -> Result<MtpDevice, Error> {
337        let devices = NusbTransport::list_mtp_devices()?;
338        let device_info = devices.into_iter().next().ok_or(Error::NoDevice)?;
339        let device = device_info.open().map_err(Error::Usb)?;
340        self.open_device(device).await
341    }
342
343    /// Open a device at a specific USB location (port).
344    ///
345    /// Use `MtpDevice::list_devices()` to get available location IDs.
346    pub async fn open_by_location(self, location_id: u64) -> Result<MtpDevice, Error> {
347        let devices = NusbTransport::list_mtp_devices()?;
348        let device_info = devices
349            .into_iter()
350            .find(|d| d.location_id == location_id)
351            .ok_or(Error::NoDevice)?;
352        let device = device_info.open().map_err(Error::Usb)?;
353        self.open_device(device).await
354    }
355
356    /// Open a device by its serial number.
357    ///
358    /// This identifies a specific physical device regardless of which USB port
359    /// it's connected to.
360    pub async fn open_by_serial(self, serial: &str) -> Result<MtpDevice, Error> {
361        let devices = NusbTransport::list_mtp_devices()?;
362        let device_info = devices
363            .into_iter()
364            .find(|d| d.serial_number.as_deref() == Some(serial))
365            .ok_or(Error::NoDevice)?;
366        let device = device_info.open().map_err(Error::Usb)?;
367        self.open_device(device).await
368    }
369
370    /// Internal: open an already-discovered device.
371    async fn open_device(self, device: nusb::Device) -> Result<MtpDevice, Error> {
372        // Open transport
373        let transport = NusbTransport::open_with_timeout(device, self.timeout).await?;
374        let transport: Arc<dyn Transport> = Arc::new(transport);
375
376        // Open session (use session ID 1)
377        let session = Arc::new(PtpSession::open(transport.clone(), 1).await?);
378
379        // Get device info
380        let device_info = session.get_device_info().await?;
381
382        let inner = Arc::new(MtpDeviceInner {
383            session,
384            device_info,
385        });
386
387        Ok(MtpDevice { inner })
388    }
389}
390
391impl Default for MtpDeviceBuilder {
392    fn default() -> Self {
393        Self::new()
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn list_devices_returns_ok() {
403        assert!(MtpDevice::list_devices().is_ok());
404    }
405
406    #[test]
407    fn builder_timeout() {
408        // Default value
409        let builder = MtpDeviceBuilder::new();
410        assert_eq!(builder.timeout, NusbTransport::DEFAULT_TIMEOUT);
411
412        // Custom value
413        let custom = MtpDeviceBuilder::new().timeout(Duration::from_secs(45));
414        assert_eq!(custom.timeout, Duration::from_secs(45));
415    }
416
417    #[test]
418    fn device_info_display() {
419        let with_serial = MtpDeviceInfo {
420            vendor_id: 0x04e8,
421            product_id: 0x6860,
422            manufacturer: Some("Samsung".to_string()),
423            product: Some("Galaxy S24".to_string()),
424            serial_number: Some("ABC123".to_string()),
425            location_id: 0x00200000,
426        };
427        let display = with_serial.display();
428        assert!(display.contains("Samsung") && display.contains("Galaxy S24"));
429        assert!(display.contains("ABC123") && display.contains("00200000"));
430
431        // Without serial
432        let no_serial = MtpDeviceInfo {
433            serial_number: None,
434            ..with_serial.clone()
435        };
436        assert!(!no_serial.display().contains("serial:"));
437
438        // Unknown manufacturer
439        let unknown = MtpDeviceInfo {
440            manufacturer: None,
441            product: None,
442            ..with_serial
443        };
444        assert!(unknown.display().contains("Unknown"));
445    }
446
447    #[tokio::test]
448    #[ignore] // Requires real MTP device
449    async fn real_device_operations() {
450        let device = MtpDevice::open_first().await.unwrap();
451        println!("Connected to: {}", device.device_info().model);
452        for storage in device.storages().await.unwrap() {
453            println!("Storage: {}", storage.info().description);
454        }
455        device.close().await.unwrap();
456    }
457}