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}