Import from an SD card or card reader on Android

Import was switched off on Android: `imports_supported` was true only for
`target_os = "linux"`, and its comment said Android has no path to read a
card by and nowhere to write the copies. Neither holds. With "all files
access" (MANAGE_EXTERNAL_STORAGE, API 30) an app reads the root of an SD
card or a USB card reader by path, `/storage/9C33-6BBD`, and the importer
only ever writes into its own staging directory, which is a plain
directory on Android too. So the engine runs unchanged; what was missing
was finding the card and the permission.

- The manifest declares MANAGE_EXTERNAL_STORAGE, and
  READ_EXTERNAL_STORAGE up to API 29 with requestLegacyExternalStorage,
  which is the same access on 28 and 29.
- Cards.java lists the mounted non-primary volumes through
  StorageManager and opens the system "All files access" page for this
  app. dr_ui::cards is the JNI bridge, through saf's helpers.
- The import page on Android asks for the permission with an "Allow
  access" button until it has it, rather than showing an empty list that
  reads as "no card", and watches for the grant so the list fills in when
  the user comes back from settings.

Google Play restricts this permission to file managers and the like;
DarkRoom is sideloaded, so that does not apply.
This commit is contained in:
2026-09-30 21:30:09 -04:00
parent fcccc2c2e0
commit caae65c78d
11 changed files with 568 additions and 115 deletions
+18 -19
View File
@@ -4,7 +4,7 @@
//! FR-CAT-10 asks for removable-volume insertion to be detected "where the
//! platform permits", which is a careful phrase and this module is why. There
//! is no portable answer: Linux has a mount table and a sysfs flag, Android
//! has neither and hands out a document tree the user picked (ARCH §6.9).
//! has neither and lists its volumes through a Java service (`dr_ui::cards`).
//! So this reports what it can and returns an empty list where it cannot,
//! and every caller must still offer the user a way to say where the card is.
//!
@@ -67,22 +67,18 @@ impl Volume {
/// where it is mounted. `false` here means the operation cannot be performed
/// however hard the user tries, and the interface should not offer it.
///
/// It is `false` on Android, for two reasons that both have to be fixed before
/// it can change:
/// It is `true` on Linux and on Android. On Android the card is read by
/// path too — `/storage/9C33-6BBD` — once the user has granted "all files
/// access", but finding it takes the platform's `StorageManager`, which is
/// Java. So [`volumes`] still answers empty there, and `dr_ui::cards` lists
/// the volumes and asks for the permission instead.
///
/// - There is no mount table to read and no path to type. Storage is reached
/// through a tree the user granted, and a removable volume appears there or
/// not at all (ARCH §6.9).
/// - Nothing implements [`WritableStorage`](crate::WritableStorage) except
/// [`LocalStorage`](crate::LocalStorage), so there is no destination to write
/// into even once a source is named.
///
/// The engine above this is already portable — it takes storage traits and
/// never a path — so what this gates is the *interface*, and it stops being
/// `false` when a SAF implementation lands rather than when the importer is
/// rewritten.
/// The engine above this takes storage traits and never a path, and the
/// importer only ever *writes* into its own staging directory, which is a
/// plain directory on every platform. So what this gates is whether a card
/// can be *read*, nothing more.
pub const fn imports_supported() -> bool {
cfg!(target_os = "linux")
cfg!(any(target_os = "linux", target_os = "android"))
}
/// Every mounted volume that might hold photographs.
@@ -130,9 +126,9 @@ fn platform_volumes() -> Vec<Volume> {
/// Everywhere else: no answer, and saying so is the honest result.
///
/// On Android the question is not merely unanswerable but wrong — storage is
/// reached through a tree the user granted, and a card appears there or not at
/// all (ARCH §6.9, FR-PLAT-AND-1).
/// On Android there is no readable mount table either; the volumes come from
/// `StorageManager` through `dr_ui::cards`, which needs the JNI this crate
/// does not have.
#[cfg(not(target_os = "linux"))]
fn platform_volumes() -> Vec<Volume> {
Vec::new()
@@ -358,7 +354,10 @@ tmpfs /run/user/1000 tmpfs rw,nosuid 0 0
// The two are different claims: an empty list means "plug one in",
// `false` here means "this cannot be done here". An interface that
// conflated them would offer a page that can never be used.
assert_eq!(imports_supported(), cfg!(target_os = "linux"));
assert_eq!(
imports_supported(),
cfg!(any(target_os = "linux", target_os = "android"))
);
if !imports_supported() {
assert!(volumes().is_empty());
}