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}