-
-
Notifications
You must be signed in to change notification settings - Fork 271
feat(android): trigger Event::Opened #1155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/mobile-multi-window
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "tao": minor | ||
| --- | ||
|
|
||
| Fire `Event::Opened` on Android, which now requires the activity to call the onNewIntent(intent) external function. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,8 +18,9 @@ use ndk::{ | |
| looper::{FdEvent, ForeignLooper, ThreadLooper}, | ||
| }; | ||
| use once_cell::sync::{Lazy, OnceCell}; | ||
| use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; | ||
| use std::{ | ||
| collections::BTreeMap, | ||
| collections::{BTreeMap, HashSet}, | ||
| ffi::{c_void, CStr, CString}, | ||
| fs::File, | ||
| io::{BufRead, BufReader}, | ||
|
|
@@ -33,6 +34,26 @@ use std::{ | |
| /// in the android project. | ||
| pub static PACKAGE: OnceCell<&str> = OnceCell::new(); | ||
|
|
||
| /// Character set for encoding text content in data URLs. | ||
| /// Encodes all control characters and special characters that might cause issues in URLs. | ||
| const DATA_URL_ENCODING_SET: &AsciiSet = &CONTROLS | ||
| .add(b' ') | ||
| .add(b'"') | ||
| .add(b'#') | ||
| .add(b'%') | ||
| .add(b'&') | ||
| .add(b'<') | ||
| .add(b'>') | ||
| .add(b'?') | ||
| .add(b'[') | ||
| .add(b'\\') | ||
| .add(b']') | ||
| .add(b'^') | ||
| .add(b'`') | ||
| .add(b'{') | ||
| .add(b'|') | ||
| .add(b'}'); | ||
|
|
||
| /// Generate JNI compilant functions that are necessary for | ||
| /// building android apps with tao. | ||
| /// | ||
|
|
@@ -91,6 +112,7 @@ macro_rules! android_binding { | |
| android_fn!($domain, $package, $activity, onActivityDestroy, [JObject]); | ||
| android_fn!($domain, $package, $activity, onActivityLowMemory, [JObject]); | ||
| android_fn!($domain, $package, $activity, onWindowFocusChanged, [JObject,i32]); | ||
| android_fn!($domain, $package, $activity, onNewIntent, [JObject]); | ||
| }}; | ||
| } | ||
|
|
||
|
|
@@ -188,6 +210,7 @@ lazy_static::lazy_static! { | |
| pub(crate) static ref CONTEXTS: Mutex<BTreeMap<ActivityId, AndroidContext>> = Mutex::new(Default::default()); | ||
| static ref WINDOW_MANAGER: Mutex<BTreeMap<ActivityId, GlobalRef>> = Mutex::new(Default::default()); | ||
| pub(crate) static ref ACTIVITY_CREATED_SENDERS: Mutex<BTreeMap<ActivityId, Sender<()>>> = Mutex::new(Default::default()); | ||
| static ref INTENT_URLS: Mutex<Vec<url::Url>> = Mutex::new(Default::default()); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using global mutable state for intent URLs. How does this interact with multi-window support? What if multiple windows exist? How does the intent get delivered to the correct window?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the intent URL is not delivered to a particular window - it is instead sent as a global event Event::Opened |
||
| } | ||
|
|
||
| static INPUT_QUEUE: Lazy<RwLock<Option<InputQueue>>> = Lazy::new(Default::default); | ||
|
|
@@ -248,6 +271,10 @@ pub fn poll_events() -> Option<Event> { | |
| } | ||
| } | ||
|
|
||
| pub fn take_intent_urls() -> Vec<url::Url> { | ||
| INTENT_URLS.lock().unwrap().drain(..).collect() | ||
| } | ||
|
|
||
| unsafe fn wake(event: Event) { | ||
| log::trace!("{:?}", event); | ||
| let size = std::mem::size_of::<Event>(); | ||
|
|
@@ -263,7 +290,8 @@ pub struct Rect { | |
| pub bottom: u32, | ||
| } | ||
|
|
||
| #[derive(Clone, Debug, Eq, PartialEq)] | ||
| // event must be copyable to be used in the event loop | ||
| #[derive(Clone, Debug, Eq, PartialEq, Copy)] | ||
| #[repr(u8)] | ||
| pub enum Event { | ||
| Start, | ||
|
|
@@ -273,9 +301,10 @@ pub enum Event { | |
| LowMemory, | ||
| WindowEvent { id: WindowId, event: WindowEvent }, | ||
| ContentRectChanged, | ||
| Opened, | ||
| } | ||
|
|
||
| #[derive(Clone, Debug, Eq, PartialEq)] | ||
| #[derive(Clone, Debug, Eq, PartialEq, Copy)] | ||
| #[repr(u8)] | ||
| pub enum WindowEvent { | ||
| Focused(bool), | ||
|
|
@@ -350,6 +379,12 @@ pub unsafe fn onActivityCreate( | |
| activity: JObject, | ||
| setup: unsafe fn(&str, JNIEnv, &ThreadLooper, GlobalRef), | ||
| ) { | ||
| let intent = env | ||
| .call_method(&activity, "getIntent", "()Landroid/content/Intent;", &[]) | ||
| .unwrap() | ||
| .l() | ||
| .unwrap(); | ||
|
|
||
| let activity_id = env | ||
| .call_method(&activity, "getId", "()I", &[]) | ||
| .unwrap() | ||
|
|
@@ -386,7 +421,7 @@ pub unsafe fn onActivityCreate( | |
| .insert(activity_id, window_manager); | ||
| let activity = env.new_global_ref(activity).unwrap(); | ||
| let vm = env.get_java_vm().unwrap(); | ||
| let env = vm.attach_current_thread_as_daemon().unwrap(); | ||
| let thread_env = vm.attach_current_thread_as_daemon().unwrap(); | ||
|
|
||
| CONTEXTS.lock().unwrap().insert( | ||
| activity_id, | ||
|
|
@@ -398,7 +433,7 @@ pub unsafe fn onActivityCreate( | |
| }, | ||
| ); | ||
| let looper = ThreadLooper::for_thread().unwrap(); | ||
| setup(PACKAGE.get().unwrap(), env, &looper, activity); | ||
| setup(PACKAGE.get().unwrap(), thread_env, &looper, activity); | ||
|
|
||
| if let Some(tx) = ACTIVITY_CREATED_SENDERS | ||
| .lock() | ||
|
|
@@ -407,6 +442,8 @@ pub unsafe fn onActivityCreate( | |
| { | ||
| let _ = tx.send(()); | ||
| } | ||
|
|
||
| handle_intent(env, intent); | ||
| } | ||
|
|
||
| pub unsafe fn resume(_: JNIEnv, _: JClass, _: JObject) { | ||
|
|
@@ -436,6 +473,203 @@ pub unsafe fn onWindowFocusChanged( | |
| wake(event); | ||
| } | ||
|
|
||
| #[allow(non_snake_case)] | ||
| pub unsafe fn onNewIntent(env: JNIEnv, _: JClass, intent: JObject) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are a number of silent failure paths in this function e.g. let Ok(action) = env.get_string(&action)
.map(|action| action.to_string_lossy().to_string())
else {
return; // Silent failure
};Should these errors be propagated or logged?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| handle_intent(env, intent); | ||
| } | ||
|
|
||
| pub unsafe fn handle_intent(mut env: JNIEnv, intent: JObject) { | ||
| let action = env | ||
| .call_method(&intent, "getAction", "()Ljava/lang/String;", &[]) | ||
| .unwrap() | ||
| .l() | ||
| .unwrap() | ||
| .into(); | ||
| let action = env | ||
| .get_string(&action) | ||
| .map(|action| action.to_string_lossy().to_string()) | ||
| .unwrap(); | ||
|
|
||
| // Only handle SEND, SEND_MULTIPLE, and VIEW actions | ||
| if action != "android.intent.action.SEND" | ||
| && action != "android.intent.action.VIEW" | ||
| && action != "android.intent.action.SEND_MULTIPLE" | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| let mut urls = HashSet::new(); | ||
|
|
||
| // Get intent type (may be null) | ||
| let intent_type = env | ||
| .call_method(&intent, "getType", "()Ljava/lang/String;", &[]) | ||
| .ok() | ||
| .and_then(|result| result.l().ok()) | ||
| .map(|jstr| jstr.into()) | ||
| .map(|intent_type| { | ||
| env | ||
| .get_string(&intent_type) | ||
| .unwrap() | ||
| .to_string_lossy() | ||
| .to_string() | ||
| }); | ||
|
|
||
| // Handle text/plain intents (EXTRA_TEXT) | ||
| if intent_type.as_deref() == Some("text/plain") { | ||
| let extra_text = env | ||
| .call_method( | ||
| &intent, | ||
| "getStringExtra", | ||
| "(Ljava/lang/String;)Ljava/lang/String;", | ||
| &[(&env.new_string("android.intent.extra.TEXT").unwrap()).into()], | ||
| ) | ||
| .ok() | ||
| .and_then(|result| result.l().ok()) | ||
| .and_then(|jstr| { | ||
| let jstr: JString = jstr.into(); | ||
| env | ||
| .get_string(&jstr) | ||
| .ok() | ||
| .map(|s| s.to_string_lossy().to_string()) | ||
| }); | ||
|
|
||
| if let Some(text) = extra_text { | ||
| if !text.is_empty() { | ||
| // Check if it's a valid URL | ||
| if let Ok(url) = url::Url::parse(&text) { | ||
| urls.insert(url); | ||
| } else { | ||
| // If not a URL, create a data URL for plain text | ||
| // Use percent encoding for the text content | ||
| let encoded = utf8_percent_encode(&text, DATA_URL_ENCODING_SET).to_string(); | ||
| if let Ok(url) = url::Url::parse(&format!("data:text/plain,{}", encoded)) { | ||
| urls.insert(url); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Handle ClipData (API >= KITKAT, which is API 19) | ||
| // We'll try to get clip data, and if it fails, we'll continue | ||
| let clip_data = env | ||
| .call_method(&intent, "getClipData", "()Landroid/content/ClipData;", &[]) | ||
| .ok() | ||
| .and_then(|result| result.l().ok()); | ||
|
|
||
| if let Some(clip_data) = clip_data { | ||
| let item_count = env | ||
| .call_method(&clip_data, "getItemCount", "()I", &[]) | ||
| .unwrap() | ||
| .i() | ||
| .unwrap(); | ||
|
|
||
| for i in 0..item_count { | ||
| let clip_item = env | ||
| .call_method( | ||
| &clip_data, | ||
| "getItemAt", | ||
| "(I)Landroid/content/ClipData$Item;", | ||
| &[i.into()], | ||
| ) | ||
| .unwrap() | ||
| .l() | ||
| .unwrap(); | ||
|
|
||
| let uri = env | ||
| .call_method(&clip_item, "getUri", "()Landroid/net/Uri;", &[]) | ||
| .ok() | ||
| .and_then(|result| result.l().ok()); | ||
|
|
||
| if let Some(uri) = uri { | ||
| let uri_string: JString = env | ||
| .call_method(&uri, "toString", "()Ljava/lang/String;", &[]) | ||
| .unwrap() | ||
| .l() | ||
| .unwrap() | ||
| .into(); | ||
| let uri_str = env | ||
| .get_string(&uri_string) | ||
| .unwrap() | ||
| .to_string_lossy() | ||
| .to_string(); | ||
| if let Ok(url) = url::Url::parse(&uri_str) { | ||
| urls.insert(url); | ||
| } else { | ||
| log::error!("failed to parse URI: {}", uri_str); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Handle EXTRA_STREAM (for file sharing) | ||
| let extras = env | ||
| .call_method(&intent, "getExtras", "()Landroid/os/Bundle;", &[]) | ||
| .ok() | ||
| .and_then(|result| result.l().ok()); | ||
|
|
||
| if let Some(extras) = extras { | ||
| let extra_stream = env | ||
| .call_method( | ||
| &extras, | ||
| "get", | ||
| "(Ljava/lang/String;)Ljava/lang/Object;", | ||
| &[(&env.new_string("android.intent.extra.STREAM").unwrap()).into()], | ||
| ) | ||
| .ok() | ||
| .and_then(|result| result.l().ok()); | ||
|
|
||
| if let Some(stream_uri) = extra_stream { | ||
| let uri_string: JString = env | ||
| .call_method(&stream_uri, "toString", "()Ljava/lang/String;", &[]) | ||
| .unwrap() | ||
| .l() | ||
| .unwrap() | ||
| .into(); | ||
| let uri_str = env | ||
| .get_string(&uri_string) | ||
| .unwrap() | ||
| .to_string_lossy() | ||
| .to_string(); | ||
| if let Ok(url) = url::Url::parse(&uri_str) { | ||
| urls.insert(url); | ||
| } else { | ||
| log::error!("failed to parse URI: {}", uri_str); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Handle getDataString() for VIEW intents (deeplinks) | ||
| if action == "android.intent.action.VIEW" { | ||
| let data_string = env | ||
| .call_method(&intent, "getDataString", "()Ljava/lang/String;", &[]) | ||
| .ok() | ||
| .and_then(|result| result.l().ok()) | ||
| .and_then(|jstr| { | ||
| let jstr: JString = jstr.into(); | ||
| env | ||
| .get_string(&jstr) | ||
| .ok() | ||
| .map(|s| s.to_string_lossy().to_string()) | ||
| }); | ||
|
|
||
| if let Some(data_str) = data_string { | ||
| if let Ok(url) = url::Url::parse(&data_str) { | ||
| urls.insert(url); | ||
| } else { | ||
| log::error!("failed to parse data string: {}", data_str); | ||
| } | ||
| } else { | ||
| log::error!("Intent data string is null"); | ||
| } | ||
| } | ||
|
|
||
| if !urls.is_empty() { | ||
| INTENT_URLS.lock().unwrap().extend(urls); | ||
| wake(Event::Opened); | ||
| } | ||
| } | ||
|
|
||
| pub unsafe fn start(_: JNIEnv, _: JClass, _: JObject) { | ||
| wake(Event::Start); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tauri converts shared text to data URLs to provide a unified
Event::Opened { urls: Vec<Url> }API across all content types (deep links, files, and text). Is there a specific reason why this pattern is used?Other cross-platforms frameworks (such as React Native) handle this by splitting out the different content types e.g.
Alternatively a tagged union could be used to handle the different content types:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
backwards compatibility
tho the new variant is probably ok, would just be a little weird to have two different paths for the same interface (intent data)