media: handle upload on android

This commit is contained in:
Fernando López Guevara
2025-07-27 16:10:47 -03:00
committed by William Casarin
parent 31ee64827a
commit 6ee2b28e70
14 changed files with 469 additions and 419 deletions

View File

@@ -51,6 +51,7 @@ bitflags = { workspace = true }
regex = "1"
chrono = { workspace = true }
indexmap = {workspace = true}
crossbeam-channel = "0.5"
[dev-dependencies]
tempfile = { workspace = true }
@@ -59,6 +60,7 @@ tokio = { workspace = true }
[target.'cfg(target_os = "android")'.dependencies]
jni = { workspace = true }
android-activity = { workspace = true }
ndk-context = "0.1"
[features]
puffin = ["puffin_egui", "dep:puffin"]

View File

@@ -1,5 +1,14 @@
use crate::platform::{file::emit_selected_file, SelectedMedia};
use jni::{
objects::{JByteArray, JClass, JObject, JObjectArray, JString},
JNIEnv,
};
use std::sync::atomic::{AtomicI32, Ordering};
use tracing::debug;
use tracing::{debug, error, info};
pub fn get_jvm() -> jni::JavaVM {
unsafe { jni::JavaVM::from_raw(ndk_context::android_context().vm().cast()) }.unwrap()
}
// Thread-safe static global
static KEYBOARD_HEIGHT: AtomicI32 = AtomicI32::new(0);
@@ -24,3 +33,80 @@ pub extern "C" fn Java_com_damus_notedeck_KeyboardHeightHelper_nativeKeyboardHei
pub fn virtual_keyboard_height() -> i32 {
KEYBOARD_HEIGHT.load(Ordering::SeqCst)
}
#[no_mangle]
pub extern "C" fn Java_com_damus_notedeck_MainActivity_nativeOnFilePickedFailed(
mut env: JNIEnv,
_class: JClass,
juri: JString,
je: JString,
) {
let _uri: String = env.get_string(&juri).unwrap().into();
let _error: String = env.get_string(&je).unwrap().into();
}
#[no_mangle]
pub extern "C" fn Java_com_damus_notedeck_MainActivity_nativeOnFilePickedWithContent(
mut env: JNIEnv,
_class: JClass,
// [display_name, size, mime_type]
juri_info: JObjectArray,
jcontent: JByteArray,
) {
debug!("File picked with content");
let display_name: Option<String> = {
let obj = env.get_object_array_element(&juri_info, 0).unwrap();
if obj.is_null() {
None
} else {
Some(env.get_string(&JString::from(obj)).unwrap().into())
}
};
if let Some(display_name) = display_name {
let length = env.get_array_length(&jcontent).unwrap() as usize;
let mut content: Vec<i8> = vec![0; length];
env.get_byte_array_region(&jcontent, 0, &mut content)
.unwrap();
debug!("selected file: {display_name:?} ({length:?} bytes)",);
emit_selected_file(SelectedMedia::from_bytes(
display_name,
content.into_iter().map(|b| b as u8).collect(),
));
} else {
error!("Received null file name");
}
}
pub fn try_open_file_picker() {
match open_file_picker() {
Ok(()) => {
info!("File picker opened successfully");
}
Err(e) => {
error!("Failed to open file picker: {}", e);
}
}
}
pub fn open_file_picker() -> std::result::Result<(), Box<dyn std::error::Error>> {
// Get the Java VM from AndroidApp
let vm = get_jvm();
// Attach current thread to get JNI environment
let mut env = vm.attach_current_thread()?;
let context = unsafe { JObject::from_raw(ndk_context::android_context().context().cast()) };
// Call the openFilePicker method on the MainActivity
env.call_method(
context,
"openFilePicker",
"()V", // Method signature: no parameters, void return
&[], // No arguments
)?;
Ok(())
}

View File

@@ -0,0 +1,99 @@
use std::{path::PathBuf, str::FromStr};
use crossbeam_channel::{unbounded, Receiver, Sender};
use once_cell::sync::Lazy;
use crate::{Error, SupportedMimeType};
#[derive(Debug)]
pub enum MediaFrom {
PathBuf(PathBuf),
Memory(Vec<u8>),
}
#[derive(Debug)]
pub struct SelectedMedia {
pub from: MediaFrom,
pub file_name: String,
pub media_type: SupportedMimeType,
}
impl SelectedMedia {
pub fn from_path(path: PathBuf) -> Result<Self, Error> {
if let Some(ex) = path.extension().and_then(|f| f.to_str()) {
let media_type = SupportedMimeType::from_extension(ex)?;
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(&format!("file.{ex}"))
.to_owned();
Ok(SelectedMedia {
from: MediaFrom::PathBuf(path),
file_name,
media_type,
})
} else {
Err(Error::Generic(format!(
"{path:?} does not have an extension"
)))
}
}
pub fn from_bytes(file_name: String, content: Vec<u8>) -> Result<Self, Error> {
if let Some(ex) = PathBuf::from_str(&file_name)
.unwrap()
.extension()
.and_then(|f| f.to_str())
{
let media_type = SupportedMimeType::from_extension(ex)?;
Ok(SelectedMedia {
from: MediaFrom::Memory(content),
file_name,
media_type,
})
} else {
Err(Error::Generic(format!(
"{file_name:?} does not have an extension"
)))
}
}
}
pub struct SelectedMediaChannel {
sender: Sender<Result<SelectedMedia, Error>>,
receiver: Receiver<Result<SelectedMedia, Error>>,
}
impl Default for SelectedMediaChannel {
fn default() -> Self {
let (sender, receiver) = unbounded();
Self { sender, receiver }
}
}
impl SelectedMediaChannel {
pub fn new_selected_file(&self, media: Result<SelectedMedia, Error>) {
let _ = self.sender.send(media);
}
pub fn try_receive(&self) -> Option<Result<SelectedMedia, Error>> {
self.receiver.try_recv().ok()
}
pub fn receive(&self) -> Option<Result<SelectedMedia, Error>> {
self.receiver.recv().ok()
}
}
pub static SELECTED_MEDIA_CHANNEL: Lazy<SelectedMediaChannel> =
Lazy::new(SelectedMediaChannel::default);
pub fn emit_selected_file(media: Result<SelectedMedia, Error>) {
SELECTED_MEDIA_CHANNEL.new_selected_file(media);
}
pub fn get_next_selected_file() -> Option<Result<SelectedMedia, Error>> {
SELECTED_MEDIA_CHANNEL.try_receive()
}

View File

@@ -1,5 +1,12 @@
use crate::{platform::file::SelectedMedia, Error};
#[cfg(target_os = "android")]
pub mod android;
pub mod file;
pub fn get_next_selected_file() -> Option<Result<SelectedMedia, Error>> {
file::get_next_selected_file()
}
const VIRT_HEIGHT: i32 = 400;

View File

@@ -238,7 +238,9 @@ impl SupportedMimeType {
{
Ok(Self { mime })
} else {
Err(Error::Generic("Unsupported mime type".to_owned()))
Err(Error::Generic(
format!("{extension} Unsupported mime type",),
))
}
}