use crate::mtp::{DeviceEvent, Storage};
use crate::ptp::{DeviceInfo, ObjectHandle, PtpSession, StorageId};
use crate::transport::{NusbTransport, Transport};
use crate::Error;
use std::sync::Arc;
use std::time::Duration;
pub(crate) struct MtpDeviceInner {
pub(crate) session: Arc<PtpSession>,
pub(crate) device_info: DeviceInfo,
}
impl MtpDeviceInner {
#[must_use]
pub fn is_android(&self) -> bool {
self.device_info
.vendor_extension_desc
.to_lowercase()
.contains("android.com")
}
}
#[derive(Clone)]
pub struct MtpDevice {
inner: Arc<MtpDeviceInner>,
}
impl MtpDevice {
pub fn builder() -> MtpDeviceBuilder {
MtpDeviceBuilder::new()
}
pub async fn open_first() -> Result<Self, Error> {
Self::builder().open_first().await
}
pub async fn open_by_location(location_id: u64) -> Result<Self, Error> {
Self::builder().open_by_location(location_id).await
}
pub async fn open_by_serial(serial: &str) -> Result<Self, Error> {
Self::builder().open_by_serial(serial).await
}
pub fn list_devices() -> Result<Vec<MtpDeviceInfo>, Error> {
let devices = NusbTransport::list_mtp_devices()?;
#[allow(unused_mut)]
let mut result: Vec<MtpDeviceInfo> = devices
.into_iter()
.map(|d| MtpDeviceInfo {
vendor_id: d.vendor_id,
product_id: d.product_id,
manufacturer: d.manufacturer,
product: d.product,
serial_number: d.serial_number,
location_id: d.location_id,
})
.collect();
#[cfg(feature = "virtual-device")]
result.extend(crate::transport::virtual_device::registry::list_virtual_devices());
Ok(result)
}
#[must_use]
pub fn device_info(&self) -> &DeviceInfo {
&self.inner.device_info
}
#[must_use]
pub fn supports_rename(&self) -> bool {
self.inner.device_info.supports_rename()
}
pub async fn storages(&self) -> Result<Vec<Storage>, Error> {
let ids = self.inner.session.get_storage_ids().await?;
let mut storages = Vec::with_capacity(ids.len());
for id in ids {
let info = self.inner.session.get_storage_info(id).await?;
storages.push(Storage::new(self.inner.clone(), id, info));
}
Ok(storages)
}
pub async fn storage(&self, id: StorageId) -> Result<Storage, Error> {
let info = self.inner.session.get_storage_info(id).await?;
Ok(Storage::new(self.inner.clone(), id, info))
}
pub async fn get_object_handles(
&self,
storage_id: StorageId,
parent: Option<ObjectHandle>,
) -> Result<Vec<ObjectHandle>, Error> {
self.inner
.session
.get_object_handles(storage_id, None, parent)
.await
}
pub async fn next_event(&self) -> Result<DeviceEvent, Error> {
match self.inner.session.poll_event().await? {
Some(container) => Ok(DeviceEvent::from_container(&container)),
None => Err(Error::Timeout),
}
}
pub async fn close(self) -> Result<(), Error> {
if let Ok(inner) = Arc::try_unwrap(self.inner) {
if let Ok(session) = Arc::try_unwrap(inner.session) {
session.close().await?;
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct MtpDeviceInfo {
pub vendor_id: u16,
pub product_id: u16,
pub manufacturer: Option<String>,
pub product: Option<String>,
pub serial_number: Option<String>,
pub location_id: u64,
}
impl MtpDeviceInfo {
#[must_use]
pub fn display(&self) -> String {
let manufacturer = self.manufacturer.as_deref().unwrap_or("Unknown");
let product = self.product.as_deref().unwrap_or("Unknown");
match &self.serial_number {
Some(serial) => format!(
"{} {} (serial: {}, location: {:08x})",
manufacturer, product, serial, self.location_id
),
None => format!(
"{} {} (location: {:08x})",
manufacturer, product, self.location_id
),
}
}
}
pub struct MtpDeviceBuilder {
timeout: Duration,
}
impl MtpDeviceBuilder {
#[must_use]
pub fn new() -> Self {
Self {
timeout: NusbTransport::DEFAULT_TIMEOUT,
}
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub async fn open_first(self) -> Result<MtpDevice, Error> {
let devices = NusbTransport::list_mtp_devices()?;
let device_info = devices.into_iter().next().ok_or(Error::NoDevice)?;
let device = device_info.open().map_err(Error::Usb)?;
self.open_device(device).await
}
pub async fn open_by_location(self, location_id: u64) -> Result<MtpDevice, Error> {
#[cfg(feature = "virtual-device")]
if let Some(config) =
crate::transport::virtual_device::registry::find_virtual_config_by_location(location_id)
{
return self.open_virtual(config).await;
}
let devices = NusbTransport::list_mtp_devices()?;
let device_info = devices
.into_iter()
.find(|d| d.location_id == location_id)
.ok_or(Error::NoDevice)?;
let device = device_info.open().map_err(Error::Usb)?;
self.open_device(device).await
}
pub async fn open_by_serial(self, serial: &str) -> Result<MtpDevice, Error> {
#[cfg(feature = "virtual-device")]
if let Some(config) =
crate::transport::virtual_device::registry::find_virtual_config_by_serial(serial)
{
return self.open_virtual(config).await;
}
let devices = NusbTransport::list_mtp_devices()?;
let device_info = devices
.into_iter()
.find(|d| d.serial_number.as_deref() == Some(serial))
.ok_or(Error::NoDevice)?;
let device = device_info.open().map_err(Error::Usb)?;
self.open_device(device).await
}
async fn open_device(self, device: nusb::Device) -> Result<MtpDevice, Error> {
let transport = NusbTransport::open_with_timeout(device, self.timeout).await?;
let transport: Arc<dyn Transport> = Arc::new(transport);
let session = Arc::new(PtpSession::open(transport.clone(), 1).await?);
let device_info = session.get_device_info().await?;
let inner = Arc::new(MtpDeviceInner {
session,
device_info,
});
Ok(MtpDevice { inner })
}
#[cfg(feature = "virtual-device")]
pub async fn open_virtual(
self,
config: crate::transport::virtual_device::config::VirtualDeviceConfig,
) -> Result<MtpDevice, Error> {
if config.storages.is_empty() {
return Err(Error::invalid_data(
"VirtualDeviceConfig requires at least one storage",
));
}
let transport = crate::transport::virtual_device::VirtualTransport::new(config);
let transport: Arc<dyn Transport> = Arc::new(transport);
let session = Arc::new(PtpSession::open(transport.clone(), 1).await?);
let device_info = session.get_device_info().await?;
let inner = Arc::new(MtpDeviceInner {
session,
device_info,
});
Ok(MtpDevice { inner })
}
}
impl Default for MtpDeviceBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn list_devices_returns_ok() {
assert!(MtpDevice::list_devices().is_ok());
}
#[test]
fn builder_timeout() {
let builder = MtpDeviceBuilder::new();
assert_eq!(builder.timeout, NusbTransport::DEFAULT_TIMEOUT);
let custom = MtpDeviceBuilder::new().timeout(Duration::from_secs(45));
assert_eq!(custom.timeout, Duration::from_secs(45));
}
#[test]
fn device_info_display() {
let with_serial = MtpDeviceInfo {
vendor_id: 0x04e8,
product_id: 0x6860,
manufacturer: Some("Samsung".to_string()),
product: Some("Galaxy S24".to_string()),
serial_number: Some("ABC123".to_string()),
location_id: 0x00200000,
};
let display = with_serial.display();
assert!(display.contains("Samsung") && display.contains("Galaxy S24"));
assert!(display.contains("ABC123") && display.contains("00200000"));
let no_serial = MtpDeviceInfo {
serial_number: None,
..with_serial.clone()
};
assert!(!no_serial.display().contains("serial:"));
let unknown = MtpDeviceInfo {
manufacturer: None,
product: None,
..with_serial
};
assert!(unknown.display().contains("Unknown"));
}
#[cfg(feature = "virtual-device")]
#[tokio::test]
async fn open_virtual_empty_storages_rejected() {
use crate::transport::virtual_device::config::VirtualDeviceConfig;
let config = VirtualDeviceConfig {
manufacturer: "TestCorp".into(),
model: "Empty".into(),
serial: "empty-001".into(),
storages: vec![],
supports_rename: false,
event_poll_interval: Duration::ZERO,
watch_backing_dirs: false,
};
let result = MtpDevice::builder().open_virtual(config).await;
match result {
Err(err) => assert!(
err.to_string().contains("at least one storage"),
"unexpected error: {}",
err
),
Ok(_) => panic!("expected error for empty storages"),
}
}
#[tokio::test]
#[ignore] async fn real_device_operations() {
let device = MtpDevice::open_first().await.unwrap();
println!("Connected to: {}", device.device_info().model);
for storage in device.storages().await.unwrap() {
println!("Storage: {}", storage.info().description);
}
device.close().await.unwrap();
}
}