Give an album a folder on the tablet, through Android's folder picker
Android's only export destination was the library on the server
(ExportTarget::available), because writing to the device goes through
the Storage Access Framework and nothing did. An album's folder on the
tablet is now chosen in the system's tree picker — which has its own
"Create new folder" — and exports are written into it with
DocumentsContract.
The picker answers through onActivityResult, and the main activity is
NativeActivity, whose result is not ours. FolderPicker is a translucent
activity that only asks: it starts ACTION_OPEN_DOCUMENT_TREE, takes a
persistable grant (a folder is chosen once and exported to for months),
leaves the URI in a static, and finishes. Rust polls it from a Slint
timer — one static call, rather than a registered native method and a
thread to deliver on.
Two things the first build on the tablet got wrong, recorded where they
are fixed:
- Our classes must be loaded through Context.getClassLoader(). The
class of what ndk_context holds is a framework class from the boot
loader, which reports every class in the APK as not found.
- What ndk_context holds is the application context, not the activity,
and starting an activity from it throws without FLAG_ACTIVITY_NEW_TASK.
Saf.write creates the document (or, under Overwrite, reopens the one of
that name with "wt" so a shorter file does not keep the old tail) and
returns the name the provider actually gave it, since SAF renames on a
collision by itself; the album records that name. A tree URI reads in
the sidebar as its folder ("Pictures/Web"), not as a content:// string.
This commit is contained in:
@@ -213,6 +213,21 @@ pub fn place(
|
||||
if destination.trim().is_empty() {
|
||||
return Err("No export folder is set. Choose an album to export to.".into());
|
||||
}
|
||||
// TRACES: FR-EXP-10 | FR-PLAT-AND-1
|
||||
// A SAF tree on Android: written through the provider, which may
|
||||
// rename on a collision, so the name it reports is the one kept.
|
||||
#[cfg(target_os = "android")]
|
||||
if destination.starts_with("content://") {
|
||||
let replace = collision_replaces(&encoded.name, destination);
|
||||
let written = crate::saf::write(
|
||||
destination,
|
||||
&encoded.name,
|
||||
mime_for(&encoded.name),
|
||||
&encoded.bytes,
|
||||
replace,
|
||||
)?;
|
||||
return Ok(Placed::Device(PathBuf::from(written)));
|
||||
}
|
||||
let dir = PathBuf::from(destination);
|
||||
std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
|
||||
let path = dir.join(&encoded.name);
|
||||
@@ -231,6 +246,30 @@ pub fn place(
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether writing `name` into a SAF folder should replace a file already
|
||||
/// there. By the time `place` runs, the batch has already chosen the name
|
||||
/// under the collision policy, so a name that is taken can only have been
|
||||
/// chosen under Overwrite.
|
||||
#[cfg(target_os = "android")]
|
||||
fn collision_replaces(name: &str, tree: &str) -> bool {
|
||||
crate::saf::exists(tree, name)
|
||||
}
|
||||
|
||||
/// The MIME type a document provider is told, from the name the encoder gave.
|
||||
#[cfg(target_os = "android")]
|
||||
fn mime_for(name: &str) -> &'static str {
|
||||
match Path::new(name)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(str::to_ascii_lowercase)
|
||||
.as_deref()
|
||||
{
|
||||
Some("png") => "image/png",
|
||||
Some("tif" | "tiff") => "image/tiff",
|
||||
_ => "image/jpeg",
|
||||
}
|
||||
}
|
||||
|
||||
/// Write bytes and their destination record into the outbox.
|
||||
fn stage(encoded: &Encoded, remote_dir: &str, outbox: &Path) -> Result<PathBuf, String> {
|
||||
std::fs::create_dir_all(outbox).map_err(|e| format!("{}: {e}", outbox.display()))?;
|
||||
@@ -983,6 +1022,10 @@ fn resolve_batch_name(
|
||||
let dir = PathBuf::from(&settings.destination);
|
||||
let taken = |name: &str| -> bool {
|
||||
match settings.target {
|
||||
#[cfg(target_os = "android")]
|
||||
dr_types::ExportTarget::Device if settings.destination.starts_with("content://") => {
|
||||
crate::saf::exists(&settings.destination, name)
|
||||
}
|
||||
dr_types::ExportTarget::Device => dir.join(name).exists(),
|
||||
// A queued export cannot see the server, and may never be able to.
|
||||
// Names are kept apart in the outbox instead — see [`stage`].
|
||||
|
||||
Reference in New Issue
Block a user