Skip to content

Commit 14b3ac3

Browse files
committed
MTP: Register late-arriving storages from Samsung
- Handle `StoreAdded` event in the MTP event loop — query the new storage, probe write capability, register it as an `MtpVolume`, and broadcast `volumes-changed` - Handle `StoreRemoved` symmetrically (unregister volume, broadcast) - Duplicate `StoreAdded` events are safely ignored if the storage is already registered - Fixes Samsung phones (and other devices) that report 0 storages at connection time and send `StoreAdded` asynchronously — previously the device would connect but never appear in the volume selector
1 parent 86b4051 commit 14b3ac3

3 files changed

Lines changed: 146 additions & 5 deletions

File tree

apps/desktop/src-tauri/src/mtp/CLAUDE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ USB unplug
4949
5050
Event loop (event_loop.rs)
5151
→ device.next_event()
52-
→ compute_diff()
53-
→ emit directory-diff (same format as local file watching)
52+
→ ObjectAdded/Removed/Changed → compute_diff() → emit directory-diff
53+
→ StoreAdded → handle_storage_added() → register MtpVolume → emit volumes-changed
54+
→ StoreRemoved → handle_storage_removed() → unregister MtpVolume → emit volumes-changed
5455
```
5556

5657
### MTP enabled/disabled toggle

apps/desktop/src-tauri/src/mtp/connection/event_loop.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,26 @@ impl MtpConnectionManager {
128128
}
129129
DeviceEvent::StorageInfoChanged { storage_id } => {
130130
debug!("MTP storage info changed: {:?} on {}", storage_id, device_id);
131-
// Could emit a storage space update event in the future
132131
}
133132
DeviceEvent::StoreAdded { storage_id } => {
134133
info!("MTP storage added: {:?} on {}", storage_id, device_id);
135-
// Could emit a storage list update event in the future
134+
let device_id = device_id.to_string();
135+
let app = app.clone();
136+
tokio::spawn(async move {
137+
connection_manager()
138+
.handle_storage_added(&device_id, storage_id.0, &app)
139+
.await;
140+
});
136141
}
137142
DeviceEvent::StoreRemoved { storage_id } => {
138143
info!("MTP storage removed: {:?} on {}", storage_id, device_id);
139-
// Could emit a storage list update event in the future
144+
let device_id = device_id.to_string();
145+
let app = app.clone();
146+
tokio::spawn(async move {
147+
connection_manager()
148+
.handle_storage_removed(&device_id, storage_id.0, &app)
149+
.await;
150+
});
140151
}
141152
DeviceEvent::DeviceInfoChanged => {
142153
debug!("MTP device info changed: {}", device_id);

apps/desktop/src-tauri/src/mtp/connection/mod.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,135 @@ impl MtpConnectionManager {
438438
.collect()
439439
}
440440

441+
/// Handles a StoreAdded event: queries the new storage, registers its volume,
442+
/// and broadcasts the change so the frontend picks it up.
443+
pub async fn handle_storage_added(&self, device_id: &str, storage_id: u32, app: &AppHandle) {
444+
let device_arc = {
445+
let devices = self.devices.lock().await;
446+
match devices.get(device_id) {
447+
Some(entry) => {
448+
// Skip if we already know about this storage (duplicate event)
449+
if entry.storages.iter().any(|s| s.id == storage_id) {
450+
debug!("handle_storage_added: storage {} already registered for {}", storage_id, device_id);
451+
return;
452+
}
453+
entry.device.clone()
454+
}
455+
None => {
456+
warn!("handle_storage_added: device {} not in registry", device_id);
457+
return;
458+
}
459+
}
460+
};
461+
462+
// Query the new storage from the device
463+
let device = match acquire_device_lock(&device_arc, device_id, "handle_storage_added").await {
464+
Ok(d) => d,
465+
Err(e) => {
466+
warn!("handle_storage_added: failed to acquire lock: {:?}", e);
467+
return;
468+
}
469+
};
470+
471+
let mtp_storage_id = mtp_rs::ptp::StorageId(storage_id);
472+
let storage = match device.storage(mtp_storage_id).await {
473+
Ok(s) => s,
474+
Err(e) => {
475+
warn!("handle_storage_added: failed to query storage {}: {:?}", storage_id, e);
476+
return;
477+
}
478+
};
479+
480+
let info = storage.info();
481+
let device_supports_write = device.device_info().supports_operation(OperationCode::SendObjectInfo);
482+
let storage_reports_read_only = !matches!(info.access_capability, mtp_rs::ptp::AccessCapability::ReadWrite);
483+
484+
let is_read_only = if !device_supports_write || storage_reports_read_only {
485+
true
486+
} else {
487+
let probe_ok = probe_write_capability(&storage, &info.description).await;
488+
if !probe_ok {
489+
info!(
490+
"Storage '{}' claims write support but probe failed - marking read-only",
491+
info.description
492+
);
493+
}
494+
!probe_ok
495+
};
496+
497+
let storage_info = MtpStorageInfo {
498+
id: storage_id,
499+
name: info.description.clone(),
500+
total_bytes: info.max_capacity,
501+
available_bytes: info.free_space_bytes,
502+
storage_type: Some(format!("{:?}", info.storage_type)),
503+
is_read_only,
504+
};
505+
506+
info!(
507+
"Registering late-arriving storage '{}' (id={}) for device {}",
508+
storage_info.name, storage_id, device_id
509+
);
510+
511+
// Release device lock before updating registry
512+
drop(device);
513+
514+
// Register the volume
515+
let volume_id = format!("{}:{}", device_id, storage_id);
516+
let volume = Arc::new(MtpVolume::new(device_id, storage_id, &storage_info.name));
517+
get_volume_manager().register(&volume_id, volume);
518+
519+
// Update the DeviceEntry's storage list
520+
{
521+
let mut devices = self.devices.lock().await;
522+
if let Some(entry) = devices.get_mut(device_id) {
523+
entry.storages.push(storage_info.clone());
524+
}
525+
}
526+
527+
// Emit updated device info so the frontend knows about the new storage
528+
let _ = app.emit(
529+
"mtp-device-connected",
530+
serde_json::json!({
531+
"deviceId": device_id,
532+
"deviceName": "",
533+
"storages": [storage_info]
534+
}),
535+
);
536+
537+
// Broadcast volume list change
538+
crate::volume_broadcast::emit_volumes_changed();
539+
}
540+
541+
/// Handles a StoreRemoved event: unregisters the volume and broadcasts the change.
542+
pub async fn handle_storage_removed(&self, device_id: &str, storage_id: u32, app: &AppHandle) {
543+
let volume_id = format!("{}:{}", device_id, storage_id);
544+
545+
// Remove from DeviceEntry
546+
{
547+
let mut devices = self.devices.lock().await;
548+
if let Some(entry) = devices.get_mut(device_id) {
549+
entry.storages.retain(|s| s.id != storage_id);
550+
}
551+
}
552+
553+
// Unregister the volume
554+
get_volume_manager().unregister(&volume_id);
555+
info!("Unregistered MTP volume: {} (storage removed)", volume_id);
556+
557+
// Emit event so frontend updates
558+
let _ = app.emit(
559+
"mtp-storage-removed",
560+
serde_json::json!({
561+
"deviceId": device_id,
562+
"storageId": storage_id
563+
}),
564+
);
565+
566+
// Broadcast volume list change
567+
crate::volume_broadcast::emit_volumes_changed();
568+
}
569+
441570
/// Queries live storage space from the device and updates the cache.
442571
///
443572
/// Returns `(total_bytes, available_bytes)` freshly read from the device,

0 commit comments

Comments
 (0)