#![allow(unsafe_code)]
use cranpose_services::{
set_platform_file_picker, FilePicker, FilePickerError, FilePickerOptions, FolderStream,
FolderStreamRef, PickedEntry, PickedEntryRef, PickedKind, PickerFuture,
};
use jni::objects::{JClass, JObject, JString, JValue};
use jni::sys::{jboolean, jlong};
use jni::{jni_sig, jni_str, EnvUnowned, Outcome};
use std::collections::HashMap;
use std::fs::File;
use std::future::Future;
use std::io::{self, Read};
use std::os::fd::FromRawFd;
use std::pin::Pin;
use std::rc::Rc;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{Mutex, OnceLock};
use std::task::{Context, Poll, Waker};
type PickResult = Result<Option<PickedEntryRef>, FilePickerError>;
struct PickedDocument {
uri: String,
name: String,
}
struct RawResult {
folder: bool,
documents: Vec<PickedDocument>,
cancelled: bool,
error: Option<String>,
}
#[derive(Default)]
struct Pending {
result: Option<RawResult>,
waker: Option<Waker>,
}
static APP: OnceLock<android_activity::AndroidApp> = OnceLock::new();
static NEXT_TOKEN: AtomicI64 = AtomicI64::new(1);
fn pending() -> &'static Mutex<HashMap<i64, Pending>> {
static PENDING: OnceLock<Mutex<HashMap<i64, Pending>>> = OnceLock::new();
PENDING.get_or_init(|| Mutex::new(HashMap::new()))
}
pub(crate) fn register(app: android_activity::AndroidApp) {
let _ = APP.set(app);
set_platform_file_picker(Rc::new(AndroidFilePicker));
}
struct AndroidFilePicker;
impl FilePicker for AndroidFilePicker {
fn pick_file(&self, _options: FilePickerOptions) -> PickerFuture<PickResult> {
present(false)
}
fn pick_folder(&self, _options: FilePickerOptions) -> PickerFuture<PickResult> {
present(true)
}
fn pick_folder_streaming(
&self,
_options: FilePickerOptions,
) -> PickerFuture<Result<Option<FolderStreamRef>, FilePickerError>> {
present_folder_stream()
}
}
#[derive(Clone, Copy)]
enum PickKind {
File,
Folder,
FolderStreaming,
}
fn present(folder: bool) -> PickerFuture<PickResult> {
let token = NEXT_TOKEN.fetch_add(1, Ordering::Relaxed);
pending()
.lock()
.expect("file picker registry poisoned")
.insert(token, Pending::default());
let kind = if folder {
PickKind::Folder
} else {
PickKind::File
};
if let Err(error) = call_activity(kind, token) {
pending()
.lock()
.expect("file picker registry poisoned")
.remove(&token);
return Box::pin(async move { Err(FilePickerError::Failed(error)) });
}
Box::pin(PickFuture { token })
}
fn call_activity(kind: PickKind, token: i64) -> Result<(), String> {
let app = APP
.get()
.ok_or_else(|| "Android file picker was not registered".to_string())?;
crate::android_jni::with_android_activity_env(app, |env, activity| {
let method = match kind {
PickKind::File => jni_str!("cranposePickFile"),
PickKind::Folder => jni_str!("cranposePickFolder"),
PickKind::FolderStreaming => jni_str!("cranposePickFolderStreaming"),
};
env.call_method(&activity, method, jni_sig!("(J)V"), &[JValue::Long(token)])
.map(|_| ())
.map_err(|error| format!("failed to launch Android picker: {error}"))
})
}
struct PickFuture {
token: i64,
}
impl Future for PickFuture {
type Output = PickResult;
fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<PickResult> {
let mut registry = pending().lock().expect("file picker registry poisoned");
let Some(slot) = registry.get_mut(&self.token) else {
return Poll::Ready(Ok(None));
};
match slot.result.take() {
Some(raw) => {
registry.remove(&self.token);
Poll::Ready(build_result(raw))
}
None => {
slot.waker = Some(context.waker().clone());
Poll::Pending
}
}
}
}
fn build_result(raw: RawResult) -> PickResult {
if raw.cancelled {
return Ok(None);
}
if let Some(error) = raw.error {
return Err(FilePickerError::Failed(error));
}
if raw.folder {
let children: Vec<PickedEntryRef> = raw
.documents
.into_iter()
.map(|document| Rc::new(UriEntry::from(document)) as PickedEntryRef)
.collect();
Ok(Some(Rc::new(FolderEntry { children })))
} else {
match raw.documents.into_iter().next() {
Some(document) => Ok(Some(Rc::new(UriEntry::from(document)))),
None => Ok(None),
}
}
}
struct UriEntry {
uri: String,
name: String,
}
impl From<PickedDocument> for UriEntry {
fn from(document: PickedDocument) -> Self {
UriEntry {
uri: document.uri,
name: document.name,
}
}
}
impl PickedEntry for UriEntry {
fn name(&self) -> String {
self.name.clone()
}
fn kind(&self) -> PickedKind {
PickedKind::File
}
fn display_path(&self) -> String {
self.uri.clone()
}
fn read_bytes(&self) -> PickerFuture<Result<Vec<u8>, FilePickerError>> {
let uri = self.uri.clone();
Box::pin(async move {
let mut file = open_content_uri(&uri)
.map_err(|error| FilePickerError::ReadFailed(error.to_string()))?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)
.map_err(|error| FilePickerError::ReadFailed(error.to_string()))?;
Ok(bytes)
})
}
fn list(&self) -> PickerFuture<Result<Vec<PickedEntryRef>, FilePickerError>> {
Box::pin(async {
Err(FilePickerError::WrongKind {
actual: "file",
expected: "folder",
})
})
}
}
struct FolderEntry {
children: Vec<PickedEntryRef>,
}
impl PickedEntry for FolderEntry {
fn name(&self) -> String {
"folder".to_string()
}
fn kind(&self) -> PickedKind {
PickedKind::Folder
}
fn display_path(&self) -> String {
String::new()
}
fn read_bytes(&self) -> PickerFuture<Result<Vec<u8>, FilePickerError>> {
Box::pin(async {
Err(FilePickerError::WrongKind {
actual: "folder",
expected: "file",
})
})
}
fn list(&self) -> PickerFuture<Result<Vec<PickedEntryRef>, FilePickerError>> {
let children = self.children.clone();
Box::pin(async move { Ok(children) })
}
}
#[derive(Default)]
struct FolderStreaming {
documents: Vec<PickedDocument>,
picked: bool,
cancelled: bool,
pick_error: Option<String>,
finished: bool,
stream_error: Option<String>,
waker: Option<Waker>,
}
fn folder_streaming() -> &'static Mutex<HashMap<i64, FolderStreaming>> {
static FOLDER_STREAMING: OnceLock<Mutex<HashMap<i64, FolderStreaming>>> = OnceLock::new();
FOLDER_STREAMING.get_or_init(|| Mutex::new(HashMap::new()))
}
fn present_folder_stream() -> PickerFuture<Result<Option<FolderStreamRef>, FilePickerError>> {
let token = NEXT_TOKEN.fetch_add(1, Ordering::Relaxed);
folder_streaming()
.lock()
.expect("folder picker registry poisoned")
.insert(token, FolderStreaming::default());
if let Err(error) = call_activity(PickKind::FolderStreaming, token) {
folder_streaming()
.lock()
.expect("folder picker registry poisoned")
.remove(&token);
return Box::pin(async move { Err(FilePickerError::Failed(error)) });
}
Box::pin(FolderPickFuture { token })
}
struct FolderPickFuture {
token: i64,
}
impl Future for FolderPickFuture {
type Output = Result<Option<FolderStreamRef>, FilePickerError>;
fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
let mut registry = folder_streaming()
.lock()
.expect("folder picker registry poisoned");
let Some(slot) = registry.get_mut(&self.token) else {
return Poll::Ready(Ok(None));
};
if slot.cancelled {
registry.remove(&self.token);
return Poll::Ready(Ok(None));
}
if let Some(error) = slot.pick_error.take() {
registry.remove(&self.token);
return Poll::Ready(Err(FilePickerError::Failed(error)));
}
if slot.picked {
return Poll::Ready(Ok(Some(
Rc::new(AndroidFolderStream { token: self.token }) as FolderStreamRef
)));
}
slot.waker = Some(context.waker().clone());
Poll::Pending
}
}
struct AndroidFolderStream {
token: i64,
}
impl FolderStream for AndroidFolderStream {
fn take_ready(&self) -> Vec<PickedEntryRef> {
let mut registry = folder_streaming()
.lock()
.expect("folder picker registry poisoned");
let Some(slot) = registry.get_mut(&self.token) else {
return Vec::new();
};
std::mem::take(&mut slot.documents)
.into_iter()
.map(|document| Rc::new(UriEntry::from(document)) as PickedEntryRef)
.collect()
}
fn is_finished(&self) -> bool {
let registry = folder_streaming()
.lock()
.expect("folder picker registry poisoned");
registry
.get(&self.token)
.map(|slot| slot.finished && slot.documents.is_empty())
.unwrap_or(true)
}
fn take_error(&self) -> Option<FilePickerError> {
let mut registry = folder_streaming()
.lock()
.expect("folder picker registry poisoned");
registry
.get_mut(&self.token)
.and_then(|slot| slot.stream_error.take())
.map(FilePickerError::Failed)
}
}
impl Drop for AndroidFolderStream {
fn drop(&mut self) {
folder_streaming()
.lock()
.expect("folder picker registry poisoned")
.remove(&self.token);
}
}
#[doc(hidden)]
#[no_mangle]
pub extern "system" fn Java_dev_cranpose_android_CranposeFilePickerActivity_nativeOnFolderPicked<
'local,
>(
mut env: EnvUnowned<'local>,
_class: JClass<'local>,
token: jlong,
cancelled: jboolean,
error: JString<'local>,
) {
let error = read_optional_jstring(&mut env, error);
let mut registry = folder_streaming()
.lock()
.expect("folder picker registry poisoned");
if let Some(slot) = registry.get_mut(&token) {
if cancelled {
slot.cancelled = true;
} else if let Some(error) = error {
slot.pick_error = Some(error);
} else {
slot.picked = true;
}
if let Some(waker) = slot.waker.take() {
waker.wake();
}
}
}
#[doc(hidden)]
#[no_mangle]
pub extern "system" fn Java_dev_cranpose_android_CranposeFilePickerActivity_nativeOnFolderEntries<
'local,
>(
mut env: EnvUnowned<'local>,
_class: JClass<'local>,
token: jlong,
entries: JString<'local>,
) -> jboolean {
let documents = read_optional_jstring(&mut env, entries)
.map(parse_documents)
.unwrap_or_default();
let mut registry = folder_streaming()
.lock()
.expect("folder picker registry poisoned");
match registry.get_mut(&token) {
Some(slot) => {
slot.documents.extend(documents);
true
}
None => false,
}
}
#[doc(hidden)]
#[no_mangle]
pub extern "system" fn Java_dev_cranpose_android_CranposeFilePickerActivity_nativeOnFolderFinished<
'local,
>(
mut env: EnvUnowned<'local>,
_class: JClass<'local>,
token: jlong,
error: JString<'local>,
) {
let error = read_optional_jstring(&mut env, error);
let mut registry = folder_streaming()
.lock()
.expect("folder picker registry poisoned");
if let Some(slot) = registry.get_mut(&token) {
slot.finished = true;
slot.stream_error = error;
}
}
pub fn open_content_uri(uri: &str) -> io::Result<File> {
let app = APP.get().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotConnected,
"Android file picker is not registered",
)
})?;
let fd = crate::android_jni::with_android_activity_env(app, |env, activity| {
let argument = env.new_string(uri).map_err(|error| error.to_string())?;
let argument: &JObject = argument.as_ref();
env.call_method(
&activity,
jni_str!("cranposeOpenUri"),
jni_sig!("(Ljava/lang/String;)I"),
&[JValue::Object(argument)],
)
.and_then(|value| value.i())
.map_err(|error| error.to_string())
})
.map_err(|error| io::Error::other(error))?;
if fd < 0 {
return Err(io::Error::other(format!(
"ContentResolver returned no descriptor for {uri}"
)));
}
Ok(unsafe { File::from_raw_fd(fd) })
}
fn deliver(token: i64, result: RawResult) {
let mut registry = pending().lock().expect("file picker registry poisoned");
if let Some(slot) = registry.get_mut(&token) {
slot.result = Some(result);
if let Some(waker) = slot.waker.take() {
waker.wake();
}
}
}
#[doc(hidden)]
#[no_mangle]
pub extern "system" fn Java_dev_cranpose_android_CranposeFilePickerActivity_nativeOnFilePicked<
'local,
>(
mut env: EnvUnowned<'local>,
_class: JClass<'local>,
token: jlong,
folder: jboolean,
entries: JString<'local>,
cancelled: jboolean,
error: JString<'local>,
) {
let documents = read_optional_jstring(&mut env, entries)
.map(parse_documents)
.unwrap_or_default();
let error = read_optional_jstring(&mut env, error);
deliver(
token,
RawResult {
folder,
documents,
cancelled,
error,
},
);
}
fn parse_documents(text: String) -> Vec<PickedDocument> {
text.lines()
.filter_map(|line| {
let mut parts = line.splitn(2, '\t');
let uri = parts.next()?;
if uri.is_empty() {
return None;
}
let name = parts.next().unwrap_or("");
Some(PickedDocument {
uri: uri.to_string(),
name: name.to_string(),
})
})
.collect()
}
fn read_optional_jstring(env: &mut EnvUnowned<'_>, value: JString<'_>) -> Option<String> {
if value.is_null() {
return None;
}
match env
.with_env(|env| -> jni::errors::Result<String> { value.try_to_string(env) })
.into_outcome()
{
Outcome::Ok(text) if !text.is_empty() => Some(text),
_ => None,
}
}