#File picker plugin on android

87 messages · Page 1 of 1 (latest)

errant trout
#

Is the file picker dualog in tauri-plugin-dialog ready to use in any capacity yet? It looks like there's at least an implementation in the github repo, but every time I try to build with cargo tauri android build, it fails with the error message method not found in FileDialogBuilder<Wry<EventLoopMessage>> on the line app().dialog().file().save_file(...). Is there a way to get an implementation of the android file picker, no matter how bleeding-edge? Or is there another workaround I should use for Android? I've tried various versions of the tauri-plugin-dialog crate, including 2.0.0-alpha.3 and referencing the github repo directly.

Secondarily, is there a version of the dialogs that uses Futures or is otherwise .awaitable? I'd like to use them in a command.

Thanks!

long vortex
errant trout
#

hmm... I assume the tauri frontend on mobile is using an embedded chrom(e/ium) browser, I wonder if I could use a local file download instead through the frontend using URL.createObjectURL(). Or, if Tauri is using an embedded web server, I wonder if I could somehow have a download link to something hosted by said web server.

#

I'll play around with the idea for a bit. If the file picker works, not having a file save is not as much of an issue for me, but it would certainly be nice.

long vortex
#

yeah, tauri uses a webview (chromium on android, webkit/safari on ios) just like on desktop

#

no actual web server tho

errant trout
#

oh, it just serves files locally, like with file://?

long vortex
#

yeah

errant trout
#

...interesting. I'll see if I can make saving a blob from the frontend work then

#

it limits the size of files, since I can't stream them as easily, but I think i could probably make it work

errant trout
#

I'm having problems actually accessing the file. The tauri file picker API gives me a path that I can't seem to access (file not found error). Do I need to convert the path somehow?

long vortex
#

maybe, yeah. The android apis return content:// urls and i have no idea how we have to handle them. Maybe you can't even use normal rust apis to use that path?

#

Maybe @lavish mirage knows more about that. I've yet to try anything with files on android.

lavish mirage
#

i'll double check, maybe it's outdated

errant trout
#

from what I've found (which could be wrong, I have minimal knowledge of the Java android API since I don't often build Android stuff and when I do it's almost never using a JVM language) that content:// url doesn't actually point to a real file system path. It looks like there's APIs in the ContentResolver to resolve the content:// url into either a Java input stream, or a C file decriptor. I may need to dive into the ndk crate to see if I can access those APIs from Rust, particularly the one that gives a file descriptor.

#

unfortunately it looks like I may need access to the Java API to get a file descriptor to pass to Rust: https://stackoverflow.com/a/58981021

errant trout
#

Is there a way to get a JNI/JVM handle from Tauri? I assume on Android tauri is initially started via JNI.

lavish mirage
#

i'm checking it atm

#

sorry, busy week

#

unfortunately docs.rs doesnt have docs for android 😦

#

@errant trout you're right you can't use the path because it's a content URI
there's a read_data input the plugin accepts, that reads the file and gives you a base64 string (yes it sucks) but it's not exposed yet because we wanted to expose a better API for this

errant trout
lavish mirage
#

might be better to do the entire operation in java if the file is that big

#

i dont know if JNI can handle it

errant trout
#

I would assume I can read it in chunks using the file descriptor

#

yeah, I'm not gonna be using jni to read a java IO stream lol

lavish mirage
#

🤣

errant trout
errant trout
#

@lavish mirage I finally got back to this project, here's what I came up with:

#

this is just for testing purposes, but it opens the dialog, converts the content URI to a Rust std::fs::File using the above function, and then displays the details and the first few bytes of the file on the frontend (to prove it can read it): ```rust
#[cfg(target_os = "android")]
#[tauri::command]
async fn open_file_dialog(window: tauri::Window, app: tauri::AppHandle, save: bool) -> Result<Option<String>, ()> {
Ok(if save {
// app.dialog().file().save_file(|| ());
None
} else {
app.dialog().file().blocking_pick_file().map(|response| {
let path = response.path.to_string_lossy().to_string();
let mut buf = [0u8; 4];
let (tx, rx) = std::sync::mpsc::channel::<Result<std::fs::File, jni::errors::Error>>();
window.with_webview(|webview| {
android::get_fd(webview.jni_handle(), path, "r", move |file| tx.send(file).unwrap() );
}).unwrap();
let result: Result<
, Box<dyn std::error::Error>> = rx.recv().unwrap()
.map_err(Into::into)
.and_then(|mut f| f.read_exact(&mut buf).map_err(Into::into));
format!(
"response: {:?}, {}",
response,
match result {
Ok(_) => format!("{buf:?}"),
Err(err) => format!("{err:?}"),
}
)
})
})
}

lavish mirage
#

that's amazing!

#

I have to try this

errant trout
#

There is a line of unsafe in there which I don't really like, but it doesn't seem like there would be a safe way to do that, and I'm pretty sure I'm holding up all of the invariants (the file descriptor just has to be open, and nobody else is going to close it).

lavish mirage
#

right

errant trout
#

It's weird, cuz it seems like it should be the kind of thing that would be in the Android NDK and thus in ndk-rs, but I couldn't find anything.

errant trout
#

it looks like there is a file save dialog in Android API 19 and above, but nut before that. Could Tauri provide support for it, or is that not planned?

lavish mirage
#

yeah we could, but we have a lot of stuff on our plate

#

i think the min API right now is 21

errant trout
#

understandable

#

I'm getting more familiar with JNI, I can probably throw something together as well

#

maybe I'll PR it

#

I must say though, after diving through the Android Java API, I do not miss OO lol

lavish mirage
#

😂😂 same

errant trout
#

unfortunately, it looks like in order to open (or, more specifically, to receive the result from) a save dialog, I need to override a method on the android Activity, which I would guess is handled by Tauri internals. I'll skip that part of my app for now, or find another way to do it (e.g., maybe I can just write files to the Downloads folder or a folder in the android userspace filesystem managed by my app but accessible by other apps, or maybe I can invoke the android share sheet.) I'll hope that Tauri gets a save dialog implemented on android by the time tauri 2 is out of pre-release though, and if you need any help with Android/JNI stuff, please do ask and maybe I can contribute something.

lavish mirage
#

which activity method?

#

maybe we can proxy it to plugins 🙂

#

we do that for onCreate, onPause, onResume

#

if it's startActivityForResult there's an example on the same plugin

errant trout
#

oh, maybe

#

I will investigate later

#

A different question: How would you recommend getting a handle to the state inside the callback for FileDialogBuilder::pick_file()?

#

and will there be an option to have that be an async function in the future?

lavish mirage
#

self.dialog.app_handle().state();

errant trout
errant trout
lavish mirage
#

FileDialogBuilder

#

i think that's what you meant

lavish mirage
errant trout
#

ok, will do

errant trout
#

is the State handle refcounted? It implements clone but idk if that's an arc style clone or a full copy

lavish mirage
errant trout
#

it looks like it's just owned by the runtime?

lavish mirage
#

yeah the reference gets cloned

errant trout
#

gotcha, that's probably the easiest to do then

#

but oh my god this is messy lol ```rust
#[cfg(target_os = "android")]
let handle_pick_file_response = |response: Option<FileResponse>| {
if let Some(response) = response {
window.with_webview(move |webview| {
android::get_file(webview.jni_handle(), response.path.to_string_lossy().to_string(), "r", move |handle| {
let message = Response::FileSelected(match handle {
Ok(handle) => {
let mut files = tauri::async_runtime::block_on(state.files.lock());
if let Some(filename) = response.name.as_ref().and_then(|name| dedup_filename(&name, &files)) {
files.push(LoadedFile {
name: filename.clone(),
handle: FileHandle::File(File::from_std(handle))
});
Ok(filename)
} else {
Err("could not find filename".to_string())
}
},
Err(err) => Err(format!("{err:?}"))
});

            tauri::async_runtime::block_on(
                state.response_tx.send(Message { return_path: call.return_path.clone(), message })
            )
                .expect("channel send error")
        })
    });
}

};

#

callback hell

#

actually, I don't even need to clone the state. A capture works just fine

lavish mirage
#

yikes

#

can't the plugin return the file handle?

#

hmm i guess you can't really serialize it

#

damn

errant trout
#

you can send the file descriptor over, it's just an integer

lavish mirage
#

that would be a lot easier

#

that’s what i was thinking

#

send the descriptor so the user can read it

#

and provide an api for js

errant trout
#

where does it need to be serialized?

#

I'm not too familiar with how plugins work

lavish mirage
#

the invoke.resolve call on the kotlin code

#

there’s a guide on the v2 docs