properly select activity, fix messaging for multi webview on android

This commit is contained in:
Lucas Nogueira
2025-10-23 15:27:28 -03:00
parent b4f5ac3037
commit df04aa8729
7 changed files with 147 additions and 99 deletions

View File

@@ -14,8 +14,11 @@ pub use jni::{
JNIEnv,
};
pub use ndk;
use ndk::looper::{FdEvent, ThreadLooper};
use std::os::fd::{AsFd, AsRawFd};
use super::{
main_pipe::{MainPipe, MAIN_PIPE},
ASSET_LOADER_DOMAIN, EVAL_CALLBACKS, IPC, ON_LOAD_HANDLER, REQUEST_HANDLER, TITLE_CHANGE_HANDLER,
URL_LOADING_OVERRIDE, WITH_ASSET_LOADER,
};
@@ -32,6 +35,7 @@ macro_rules! android_binding {
($domain:ident, $package:ident, $wry:path) => {{
use $wry::{android_setup as _, prelude::*};
android_fn!($domain, $package, Rust, wryCreate, []);
android_fn!(
$domain,
$package,
@@ -246,6 +250,27 @@ fn handle_request(
Ok(*JObject::null())
}
#[allow(non_snake_case)]
pub unsafe fn wryCreate(env: JNIEnv, _: JClass) {
let mut main_pipe = MainPipe { env };
let looper = ThreadLooper::for_thread().unwrap();
looper
.add_fd_with_callback(MAIN_PIPE[0].as_fd(), FdEvent::INPUT, move |fd, _event| {
let size = std::mem::size_of::<bool>();
let mut wake = false;
if libc::read(fd.as_raw_fd(), &mut wake as *mut _ as *mut _, size) == size as libc::ssize_t {
// unregister itself on errors
main_pipe.recv().is_ok()
} else {
// unregister itself
false
}
})
.unwrap();
}
#[allow(non_snake_case)]
pub unsafe fn onWebviewDestroy(mut env: JNIEnv, _: JClass, activity: JObject, webview_id: JString) {
let activity_id = env

View File

@@ -27,6 +27,7 @@ object Rust {
@JvmStatic external fun pause()
@JvmStatic external fun stop()
@JvmStatic external fun wryCreate()
@JvmStatic external fun onWebviewDestroy(activity: WryActivity, webviewId: String)
@JvmStatic external fun ipc(webviewId: String, url: String, message: String)

View File

@@ -15,7 +15,6 @@ import androidx.webkit.WebViewAssetLoader
class RustWebViewClient(webView: RustWebView, context: Context): WebViewClient() {
private val interceptedState = mutableMapOf<String, Boolean>()
var currentUrl: String = "about:blank"
private var lastInterceptedUrl: Uri? = null
private var pendingUrlRedirect: String? = null
private val assetLoader = WebViewAssetLoader.Builder()
@@ -35,12 +34,10 @@ class RustWebViewClient(webView: RustWebView, context: Context): WebViewClient()
return null
}
lastInterceptedUrl = request.url
return if (Rust.withAssetLoader((view as RustWebView).id)) {
assetLoader.shouldInterceptRequest(request.url)
} else {
val rustWebview = view as RustWebView;
val response = Rust.handleRequest(rustWebview.id, request, rustWebview.isDocumentStartScriptEnabled)
val response = Rust.handleRequest(view.id, request, view.isDocumentStartScriptEnabled)
interceptedState[request.url.toString()] = response != null
return response
}
@@ -76,13 +73,17 @@ class RustWebViewClient(webView: RustWebView, context: Context): WebViewClient()
// we get a net::ERR_CONNECTION_REFUSED when an external URL redirects to a custom protocol
// e.g. oauth flow, because shouldInterceptRequest is not called on redirects
// so we must force retry here with loadUrl() to get a chance of the custom protocol to kick in
if (error.errorCode == ERROR_CONNECT && request.isForMainFrame && request.url != lastInterceptedUrl) {
//
// we also get a net::ERR_CONNECTION_REFUSED when a second webview tries to load http://tauri.localhost
// so we retry the currentUrl regardless. We do not have a timeout yet due to the amount of retries needed to make it work
// but we might add a timeout in the future
if (error.errorCode == ERROR_CONNECT) {
// prevent the default error page from showing
view.stopLoading()
// without this initial loadUrl the app is stuck
view.loadUrl(request.url.toString())
view.loadUrl(currentUrl)
// ensure the URL is actually loaded - for some reason there's a race condition and we need to call loadUrl() again later
pendingUrlRedirect = request.url.toString()
pendingUrlRedirect = currentUrl
} else {
super.onReceivedError(view, request, error)
}

View File

@@ -5,6 +5,7 @@
package {{package}}
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.webkit.WebView
@@ -20,6 +21,7 @@ object WryLifecycleObserver : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
Rust.create()
Rust.wryCreate()
}
override fun onStart(owner: LifecycleOwner) {
@@ -90,7 +92,7 @@ abstract class WryActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
id = savedInstanceState?.getInt(ACTIVITY_ID_KEY) ?: hashCode()
id = savedInstanceState?.getInt(ACTIVITY_ID_KEY) ?: intent.extras?.getInt(ACTIVITY_ID_KEY) ?: hashCode()
ProcessLifecycleOwner.get().lifecycle.addObserver(WryLifecycleObserver)
Rust.onActivityCreate(this)
}
@@ -129,5 +131,13 @@ abstract class WryActivity : AppCompatActivity() {
return Class.forName(name)
}
fun startActivity(cls: Class<*>): Int {
val intent = Intent(this, cls)
val id = kotlin.random.Random.nextInt()
intent.putExtra(ACTIVITY_ID_KEY, id)
startActivity(intent)
return id
}
{{class-extension}}
}

View File

@@ -12,6 +12,7 @@
void setWebView({{package-unescaped}}.RustWebView);
java.lang.Class getAppClass(...);
java.lang.String getVersion();
int startActivity(...);
}
-keep class {{package-unescaped}}.Ipc {

View File

@@ -7,10 +7,12 @@ use crossbeam_channel::*;
use jni::{
errors::Result as JniResult,
objects::{GlobalRef, JMap, JObject, JString},
JNIEnv,
JNIEnv, JavaVM,
};
use once_cell::sync::Lazy;
use std::{
collections::BTreeMap,
ffi::c_void,
os::unix::prelude::*,
sync::{Arc, Mutex},
};
@@ -19,27 +21,40 @@ use super::{find_class, EvalCallback, WebviewId, EVAL_CALLBACKS, EVAL_ID_GENERAT
pub type ActivityId = i32;
static CHANNEL: Lazy<(
Sender<(ActivityId, WebViewMessage)>,
Receiver<(ActivityId, WebViewMessage)>,
)> = Lazy::new(|| bounded(8));
pub static MAIN_PIPE: Lazy<[OwnedFd; 2]> = Lazy::new(|| {
let mut pipe: [RawFd; 2] = Default::default();
unsafe { libc::pipe(pipe.as_mut_ptr()) };
unsafe { pipe.map(|fd| OwnedFd::from_raw_fd(fd)) }
});
#[derive(Clone)]
pub struct ActivityProxy {
pub channel: (Sender<WebViewMessage>, Receiver<WebViewMessage>),
pub pipe: Arc<[OwnedFd; 2]>,
pub activity: GlobalRef,
pub window_manager: GlobalRef,
pub webview: Option<GlobalRef>,
pub webchrome_client: GlobalRef,
pub java_vm: *mut c_void,
}
unsafe impl Send for ActivityProxy {}
impl ActivityProxy {
pub fn new(activity: GlobalRef, webchrome_client: GlobalRef) -> Self {
let mut pipe: [RawFd; 2] = Default::default();
unsafe { libc::pipe(pipe.as_mut_ptr()) };
let pipe = unsafe { pipe.map(|fd| OwnedFd::from_raw_fd(fd)) };
let channel = bounded(8);
pub fn new(
vm: JavaVM,
activity: GlobalRef,
window_manager: GlobalRef,
webchrome_client: GlobalRef,
) -> Self {
Self {
channel,
pipe: Arc::new(pipe),
activity,
window_manager,
webview: None,
webchrome_client,
java_vm: vm.get_java_vm_pointer() as *mut _,
}
}
}
@@ -54,24 +69,42 @@ pub fn activity_proxy(id: ActivityId) -> Option<ActivityProxy> {
}
pub fn register_activity_proxy(
vm: JavaVM,
id: ActivityId,
activity: GlobalRef,
window_manager: GlobalRef,
webchrome_client: GlobalRef,
) -> ActivityProxy {
) {
let mut activity_proxy = ACTIVITY_PROXY.lock().unwrap();
if let Some(proxy) = activity_proxy.get_mut(&id) {
proxy.activity = activity;
proxy.window_manager = window_manager;
proxy.webchrome_client = webchrome_client;
proxy.clone()
proxy.java_vm = vm.get_java_vm_pointer() as *mut _;
} else {
let proxy = ActivityProxy::new(activity, webchrome_client);
let proxy = ActivityProxy::new(vm, activity, window_manager, webchrome_client);
activity_proxy.insert(id, proxy.clone());
proxy
}
}
pub fn last_activity_id() -> Option<ActivityId> {
ACTIVITY_PROXY.lock().unwrap().keys().next_back().cloned()
pub fn activity_id_for_window_manager(window_manager: JObject) -> Option<ActivityId> {
for (activity_id, proxy) in ACTIVITY_PROXY.lock().unwrap().iter() {
let vm = unsafe { JavaVM::from_raw(proxy.java_vm.cast()) }.unwrap();
let mut env = vm.attach_current_thread_as_daemon().unwrap();
let equals = env
.call_method(
proxy.window_manager.as_obj(),
"equals",
"(Ljava/lang/Object;)Z",
&[(&window_manager).into()],
)
.and_then(|v| v.z())
.unwrap_or_default();
if equals {
return Some(*activity_id);
}
}
None
}
pub fn first_activity_id() -> Option<ActivityId> {
@@ -89,28 +122,17 @@ pub fn get_webview(activity_id: ActivityId) -> Option<GlobalRef> {
.cloned()
}
pub enum MainPipeState {
Alive,
Destroyed,
}
pub struct MainPipe<'a> {
pub env: JNIEnv<'a>,
pub activity_id: ActivityId,
}
impl<'a> MainPipe<'a> {
pub(crate) fn send(activity_id: ActivityId, message: WebViewMessage) {
let size = std::mem::size_of::<bool>();
let Some(proxy) = activity_proxy(activity_id) else {
#[cfg(debug_assertions)]
eprintln!("no activity proxy found for activity id: {activity_id}");
return;
};
if let Ok(()) = proxy.channel.0.send(message) {
if CHANNEL.0.send((activity_id, message)).is_ok() {
unsafe {
libc::write(
proxy.pipe[1].as_raw_fd(),
MAIN_PIPE[1].as_raw_fd(),
&true as *const _ as *const _,
size,
)
@@ -118,25 +140,16 @@ impl<'a> MainPipe<'a> {
}
}
pub fn recv(&mut self) -> JniResult<MainPipeState> {
let Some(proxy) = activity_proxy(self.activity_id) else {
#[cfg(debug_assertions)]
eprintln!(
"no activity proxy found for activity id: {}",
self.activity_id
);
return Ok(MainPipeState::Destroyed);
};
let rx = proxy.channel.1;
if let Ok(message) = rx.recv() {
pub fn recv(&mut self) -> JniResult<()> {
if let Ok((activity_id, message)) = CHANNEL.1.recv() {
match message {
WebViewMessage::CreateWebView(attrs) => {
let Some((activity, web_chrome_client)) = activity_proxy(self.activity_id)
.map(|p| (p.activity.clone(), p.webchrome_client.clone()))
let Some((activity, web_chrome_client)) =
activity_proxy(activity_id).map(|p| (p.activity.clone(), p.webchrome_client.clone()))
else {
#[cfg(debug_assertions)]
eprintln!("no activity found for activity id: {}", self.activity_id);
return Ok(MainPipeState::Destroyed);
eprintln!("no activity found for activity id: {}", activity_id);
return Ok(());
};
let CreateWebViewAttributes {
url,
@@ -318,13 +331,13 @@ impl<'a> MainPipe<'a> {
ACTIVITY_PROXY
.lock()
.unwrap()
.get_mut(&self.activity_id)
.get_mut(&activity_id)
.unwrap()
.webview
.replace(webview);
}
WebViewMessage::Eval(script, callback) => {
if let Some(webview) = get_webview(self.activity_id) {
if let Some(webview) = get_webview(activity_id) {
let id = EVAL_ID_GENERATOR.next() as i32;
#[cfg(feature = "tracing")]
@@ -358,12 +371,12 @@ impl<'a> MainPipe<'a> {
}
}
WebViewMessage::SetBackgroundColor(background_color) => {
if let Some(webview) = get_webview(self.activity_id) {
if let Some(webview) = get_webview(activity_id) {
set_background_color(&mut self.env, webview.as_obj(), background_color)?;
}
}
WebViewMessage::GetWebViewVersion(tx) => {
if let Some(activity) = activity_proxy(self.activity_id).map(|p| p.activity.clone()) {
if let Some(activity) = activity_proxy(activity_id).map(|p| p.activity.clone()) {
match self
.env
.call_method(activity, "getVersion", "()Ljava/lang/String;", &[])
@@ -385,7 +398,7 @@ impl<'a> MainPipe<'a> {
}
}
WebViewMessage::GetUrl(tx) => {
if let Some(webview) = get_webview(self.activity_id) {
if let Some(webview) = get_webview(activity_id) {
let url = self
.env
.call_method(webview.as_obj(), "getUrl", "()Ljava/lang/String;", &[])
@@ -403,7 +416,7 @@ impl<'a> MainPipe<'a> {
}
}
WebViewMessage::Jni(f) => {
match activity_proxy(self.activity_id).map(|p| (p.activity.clone(), p.webview.clone())) {
match activity_proxy(activity_id).map(|p| (p.activity.clone(), p.webview.clone())) {
Some((activity, Some(webview))) => {
f(&mut self.env, &activity, webview.as_obj());
}
@@ -416,31 +429,31 @@ impl<'a> MainPipe<'a> {
}
}
WebViewMessage::LoadUrl(url, headers) => {
if let Some(webview) = get_webview(self.activity_id) {
if let Some(webview) = get_webview(activity_id) {
let url = self.env.new_string(url)?;
load_url(&mut self.env, webview.as_obj(), &url, headers, false)?;
}
}
WebViewMessage::ClearAllBrowsingData => {
if let Some(webview) = get_webview(self.activity_id) {
if let Some(webview) = get_webview(activity_id) {
self
.env
.call_method(webview, "clearAllBrowsingData", "()V", &[])?;
}
}
WebViewMessage::LoadHtml(html) => {
if let Some(webview) = get_webview(self.activity_id) {
if let Some(webview) = get_webview(activity_id) {
let html = self.env.new_string(html)?;
load_html(&mut self.env, webview.as_obj(), &html)?;
}
}
WebViewMessage::Reload => {
if let Some(webview) = get_webview(self.activity_id) {
if let Some(webview) = get_webview(activity_id) {
reload(&mut self.env, webview.as_obj())?;
}
}
WebViewMessage::GetCookies(tx, url) => {
if let Some(webview) = get_webview(self.activity_id) {
if let Some(webview) = get_webview(activity_id) {
let url = self.env.new_string(url)?;
let cookies = self
.env
@@ -478,12 +491,11 @@ impl<'a> MainPipe<'a> {
// e.g. rotation, multi-window mode change, etc
if !is_changing_configurations {
super::destroy_webview(activity_id, &webview_id);
return Ok(MainPipeState::Destroyed);
}
}
}
}
Ok(MainPipeState::Alive)
Ok(())
}
}

View File

@@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
use super::{PageLoadEvent, WebViewAttributes, RGBA};
use crate::{RequestAsyncResponder, Result};
use crate::{Error, RequestAsyncResponder, Result};
use base64::{engine::general_purpose, Engine};
use crossbeam_channel::*;
use html5ever::{interface::QualName, namespace_url, ns, tendril::TendrilSink, LocalName};
@@ -17,14 +17,13 @@ use jni::{
JNIEnv,
};
use kuchiki::NodeRef;
use ndk::looper::{FdEvent, ThreadLooper};
use ndk::looper::ThreadLooper;
use once_cell::sync::OnceCell;
use raw_window_handle::HasWindowHandle;
use sha2::{Digest, Sha256};
use std::{
borrow::Cow,
collections::HashMap,
os::fd::{AsFd as _, AsRawFd as _},
sync::{mpsc::channel, Mutex},
time::Duration,
};
@@ -32,8 +31,8 @@ use std::{
pub(crate) mod binding;
mod main_pipe;
use main_pipe::{
first_activity_id, last_activity_id, register_activity_proxy, ActivityId,
CreateWebViewAttributes, MainPipe, MainPipeState, WebViewMessage,
activity_id_for_window_manager, first_activity_id, register_activity_proxy, ActivityId,
CreateWebViewAttributes, MainPipe, WebViewMessage,
};
use crate::util::Counter;
@@ -113,17 +112,31 @@ pub fn destroy_webview(activity_id: ActivityId, webview_id: &WebviewId) {
pub unsafe fn android_setup(
package: &str,
mut env: JNIEnv,
looper: &ThreadLooper,
_looper: &ThreadLooper,
activity: GlobalRef,
) {
PACKAGE.get_or_init(move || package.to_string());
let vm = env.get_java_vm().unwrap();
let activity_id = env
.call_method(activity.as_obj(), "getId", "()I", &[])
.unwrap()
.i()
.unwrap();
let window_manager = env
.call_method(
&activity,
"getWindowManager",
"()Landroid/view/WindowManager;",
&[],
)
.unwrap()
.l()
.unwrap();
let window_manager = env.new_global_ref(window_manager).unwrap();
// we must create the WebChromeClient here because it calls `registerForActivityResult`,
// which gives an `LifecycleOwners must call register before they are STARTED.` error when called outside the onCreate hook
let rust_webchrome_client_class = find_class(
@@ -142,36 +155,13 @@ pub unsafe fn android_setup(
let webchrome_client = env.new_global_ref(webchrome_client).unwrap();
let activity_proxy = register_activity_proxy(activity_id, activity, webchrome_client);
register_activity_proxy(vm, activity_id, activity, window_manager, webchrome_client);
if let Some(webview_attributes) = WEBVIEW_ATTRIBUTES.lock().unwrap().get(&activity_id) {
MainPipe::send(
activity_id,
WebViewMessage::CreateWebView(webview_attributes.clone()),
);
} else {
let mut main_pipe = MainPipe { env, activity_id };
looper
.add_fd_with_callback(
activity_proxy.pipe[0].as_fd(),
FdEvent::INPUT,
move |fd, _event| {
let size = std::mem::size_of::<bool>();
let mut wake = false;
if libc::read(fd.as_raw_fd(), &mut wake as *mut _ as *mut _, size)
== size as libc::ssize_t
{
let res = main_pipe.recv();
// unregister itself on errors or destroy event
matches!(res, Ok(MainPipeState::Alive))
} else {
// unregister itself
false
}
},
)
.unwrap();
}
}
@@ -182,19 +172,27 @@ pub(crate) struct InnerWebView {
impl InnerWebView {
pub fn new_as_child(
_window: &impl HasWindowHandle,
window: &impl HasWindowHandle,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
) -> Result<Self> {
Self::new(_window, attributes, pl_attrs)
Self::new(window, attributes, pl_attrs)
}
pub fn new(
_window: &impl HasWindowHandle,
window: &impl HasWindowHandle,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
) -> Result<Self> {
let activity_id = last_activity_id().expect("no available activity");
let window_manager = match window.window_handle()?.as_raw() {
raw_window_handle::RawWindowHandle::AndroidNdk(window_manager) => {
window_manager.a_native_window
}
_ => return Err(Error::UnsupportedWindowHandle),
};
let window_manager = unsafe { JObject::from_raw(window_manager.as_ptr().cast()) };
let activity_id =
activity_id_for_window_manager(window_manager).expect("no available activity");
let WebViewAttributes {
url,
html,