From b4f5ac3037fea6dedc0cfff64f9196bcce51147c Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Wed, 22 Oct 2025 14:50:27 -0300 Subject: [PATCH 1/9] feat(android): setup multi-webview support (activity embedding) --- Cargo.lock | 1 + Cargo.toml | 1 + src/android/binding.rs | 207 +++++++----- src/android/kotlin/Ipc.kt | 12 +- src/android/kotlin/Rust.kt | 43 +++ src/android/kotlin/RustWebChromeClient.kt | 4 +- src/android/kotlin/RustWebView.kt | 9 +- src/android/kotlin/RustWebViewClient.kt | 27 +- src/android/kotlin/WryActivity.kt | 88 +++--- src/android/main_pipe.rs | 254 ++++++++++----- src/android/mod.rs | 367 +++++++++++++--------- src/error.rs | 3 + src/lib.rs | 26 +- 13 files changed, 652 insertions(+), 390 deletions(-) create mode 100644 src/android/kotlin/Rust.kt diff --git a/Cargo.lock b/Cargo.lock index ec5edb8f9..792528fd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4146,6 +4146,7 @@ dependencies = [ "javascriptcore-rs", "jni", "kuchikiki", + "lazy_static", "libc", "ndk", "objc2 0.6.0", diff --git a/Cargo.toml b/Cargo.toml index 79bd917b6..ecf71860a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -203,6 +203,7 @@ jni = "0.21" ndk = "0.9" tao-macros = "0.1" libc = "0.2" +lazy_static = "1" [dev-dependencies] pollster = "0.4.0" diff --git a/src/android/binding.rs b/src/android/binding.rs index 8e7156834..fd1489320 100644 --- a/src/android/binding.rs +++ b/src/android/binding.rs @@ -32,12 +32,18 @@ macro_rules! android_binding { ($domain:ident, $package:ident, $wry:path) => {{ use $wry::{android_setup as _, prelude::*}; - android_fn!($domain, $package, WryActivity, onActivityDestroy, [JObject]); + android_fn!( + $domain, + $package, + Rust, + onWebviewDestroy, + [JObject, JString] + ); android_fn!( $domain, $package, - RustWebViewClient, + Rust, handleRequest, [JString, JObject, jboolean], jobject @@ -45,57 +51,37 @@ macro_rules! android_binding { android_fn!( $domain, $package, - RustWebViewClient, + Rust, withAssetLoader, - [], + [JString], jboolean ); android_fn!( $domain, $package, - RustWebViewClient, + Rust, assetLoaderDomain, - [], - jstring - ); - android_fn!( - $domain, - $package, - RustWebViewClient, - shouldOverride, [JString], - jboolean + jstring ); android_fn!( $domain, $package, - RustWebView, + Rust, shouldOverride, - [JString], + [JString, JString], jboolean ); - android_fn!($domain, $package, RustWebView, onEval, [jint, JString]); - android_fn!( - $domain, - $package, - RustWebViewClient, - onPageLoading, - [JString] - ); - android_fn!( - $domain, - $package, - RustWebViewClient, - onPageLoaded, - [JString] - ); - android_fn!($domain, $package, Ipc, ipc, [JString, JString]); + android_fn!($domain, $package, Rust, onEval, [JString, jint, JString]); + android_fn!($domain, $package, Rust, onPageLoading, [JString, JString]); + android_fn!($domain, $package, Rust, onPageLoaded, [JString, JString]); + android_fn!($domain, $package, Rust, ipc, [JString, JString, JString]); android_fn!( $domain, $package, - RustWebChromeClient, + Rust, handleReceivedTitle, - [JObject, JString], + [JString, JString], ); }}; } @@ -106,7 +92,10 @@ fn handle_request( request: JObject, is_document_start_script_enabled: jboolean, ) -> JniResult { - if let Some(handler) = REQUEST_HANDLER.lock().unwrap().as_ref() { + let webview_id = env.get_string(&webview_id)?; + let webview_id = webview_id.to_str().ok().unwrap_or_default(); + + if let Some(handler) = REQUEST_HANDLER.lock().unwrap().get(webview_id) { #[cfg(feature = "tracing")] let span = tracing::info_span!(parent: None, "wry::custom_protocol::handle", uri = tracing::field::Empty).entered(); @@ -159,16 +148,13 @@ fn handle_request( let final_request = match request_builder.body(Vec::new()) { Ok(req) => req, - Err(e) => { + Err(_e) => { #[cfg(feature = "tracing")] - tracing::warn!("Failed to build response: {}", e); + tracing::warn!("Failed to build response: {_e}"); return Ok(*JObject::null()); } }; - let webview_id = env.get_string(&webview_id)?; - let webview_id = webview_id.to_str().ok().unwrap_or_default(); - let response = { #[cfg(feature = "tracing")] let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered(); @@ -190,9 +176,9 @@ fn handle_request( } else { None }; - if let Some(err) = status_err { + if let Some(_err) = status_err { #[cfg(feature = "tracing")] - tracing::warn!("{}", err); + tracing::warn!("{_err}"); return Ok(*JObject::null()); } @@ -261,8 +247,33 @@ fn handle_request( } #[allow(non_snake_case)] -pub unsafe fn onActivityDestroy(_: JNIEnv, _: JClass, _: JObject) { - super::MainPipe::send(super::WebViewMessage::OnDestroy); +pub unsafe fn onWebviewDestroy(mut env: JNIEnv, _: JClass, activity: JObject, webview_id: JString) { + let activity_id = env + .call_method(&activity, "getId", "()I", &[]) + .unwrap() + .i() + .unwrap(); + + let webview_id = env + .get_string(&webview_id) + .unwrap() + .to_string_lossy() + .to_string(); + + let is_changing_configurations = env + .call_method(&activity, "isChangingConfigurations", "()Z", &[]) + .unwrap() + .z() + .unwrap(); + + super::MainPipe::send( + activity_id, + super::WebViewMessage::OnDestroy { + activity_id, + webview_id, + is_changing_configurations, + }, + ); } #[allow(non_snake_case)] @@ -280,33 +291,44 @@ pub unsafe fn handleRequest( is_document_start_script_enabled, ) { Ok(response) => response, - Err(e) => { + Err(_e) => { #[cfg(feature = "tracing")] - tracing::warn!("Failed to handle request: {}", e); + tracing::warn!("Failed to handle request: {_e}"); JObject::null().as_raw() } } } #[allow(non_snake_case)] -pub unsafe fn shouldOverride(mut env: JNIEnv, _: JClass, url: JString) -> jboolean { +pub unsafe fn shouldOverride( + mut env: JNIEnv, + _: JClass, + webview_id: JString, + url: JString, +) -> jboolean { match env.get_string(&url) { Ok(url) => { let url = url.to_string_lossy().to_string(); + + let Ok(webview_id) = env.get_string(&webview_id) else { + return false.into(); + }; + let webview_id = webview_id.to_str().ok().unwrap_or_default(); + URL_LOADING_OVERRIDE .lock() .unwrap() - .as_ref() + .get(webview_id) // We negate the result of the function because the logic for the android // client is different from how the navigation_handler is defined. // // https://developer.android.com/reference/android/webkit/WebViewClient#shouldOverrideUrlLoading(android.webkit.WebView,%20android.webkit.WebResourceRequest) .map(|f| !(f.handler)(url)) - .unwrap_or(false) + .unwrap_or_default() } - Err(e) => { + Err(_e) => { #[cfg(feature = "tracing")] - tracing::warn!("Failed to parse JString: {}", e); + tracing::warn!("Failed to parse JString: {_e}"); false } } @@ -314,7 +336,7 @@ pub unsafe fn shouldOverride(mut env: JNIEnv, _: JClass, url: JString) -> jboole } #[allow(non_snake_case)] -pub unsafe fn onEval(mut env: JNIEnv, _: JClass, id: jint, result: JString) { +pub unsafe fn onEval(mut env: JNIEnv, _: JClass, _webview_id: JString, id: jint, result: JString) { match env.get_string(&result) { Ok(result) => { if let Some(cb) = EVAL_CALLBACKS @@ -326,56 +348,75 @@ pub unsafe fn onEval(mut env: JNIEnv, _: JClass, id: jint, result: JString) { cb(result.into()); } } - Err(e) => { + Err(_e) => { #[cfg(feature = "tracing")] - tracing::warn!("Failed to parse JString: {}", e); + tracing::warn!("Failed to parse JString: {_e}"); } } } -pub unsafe fn ipc(mut env: JNIEnv, _: JClass, url: JString, body: JString) { - match (env.get_string(&url), env.get_string(&body)) { - (Ok(url), Ok(body)) => { +pub unsafe fn ipc(mut env: JNIEnv, _: JClass, webview_id: JString, url: JString, body: JString) { + match ( + env.get_string(&url), + env.get_string(&body), + env.get_string(&webview_id), + ) { + (Ok(url), Ok(body), Ok(webview_id)) => { #[cfg(feature = "tracing")] let _span = tracing::info_span!(parent: None, "wry::ipc::handle").entered(); let url = url.to_string_lossy().to_string(); let body = body.to_string_lossy().to_string(); - if let Some(ipc) = IPC.lock().unwrap().as_ref() { + let webview_id = webview_id.to_string_lossy().to_string(); + if let Some(ipc) = IPC.lock().unwrap().get(&webview_id) { (ipc.handler)(Request::builder().uri(url).body(body).unwrap()) } } - (Err(e), _) | (_, Err(e)) => { + (Err(_e), _, _) | (_, Err(_e), _) | (_, _, Err(_e)) => { #[cfg(feature = "tracing")] - tracing::warn!("Failed to parse JString: {}", e) + tracing::warn!("Failed to parse JString: {_e}") } } } #[allow(non_snake_case)] -pub unsafe fn handleReceivedTitle(mut env: JNIEnv, _: JClass, _webview: JObject, title: JString) { - match env.get_string(&title) { - Ok(title) => { +pub unsafe fn handleReceivedTitle(mut env: JNIEnv, _: JClass, webview_id: JString, title: JString) { + match (env.get_string(&title), env.get_string(&webview_id)) { + (Ok(title), Ok(webview_id)) => { let title = title.to_string_lossy().to_string(); - if let Some(title_handler) = TITLE_CHANGE_HANDLER.lock().unwrap().as_ref() { + let webview_id = webview_id.to_string_lossy().to_string(); + if let Some(title_handler) = TITLE_CHANGE_HANDLER.lock().unwrap().get(&webview_id) { (title_handler.handler)(title) } } - Err(e) => { + (Err(_e), _) | (_, Err(_e)) => { #[cfg(feature = "tracing")] - tracing::warn!("Failed to parse JString: {}", e) + tracing::warn!("Failed to parse JString: {_e}") } } } #[allow(non_snake_case)] -pub unsafe fn withAssetLoader(_: JNIEnv, _: JClass) -> jboolean { - (*WITH_ASSET_LOADER.lock().unwrap().as_ref().unwrap_or(&false)).into() +pub unsafe fn withAssetLoader(mut env: JNIEnv, _: JClass, webview_id: JString) -> jboolean { + let Ok(webview_id) = env.get_string(&webview_id) else { + return false.into(); + }; + let webview_id = webview_id.to_str().ok().unwrap_or_default(); + (*WITH_ASSET_LOADER + .lock() + .unwrap() + .get(webview_id) + .unwrap_or(&false)) + .into() } #[allow(non_snake_case)] -pub unsafe fn assetLoaderDomain(env: JNIEnv, _: JClass) -> jstring { - if let Some(domain) = ASSET_LOADER_DOMAIN.lock().unwrap().as_ref() { +pub unsafe fn assetLoaderDomain(mut env: JNIEnv, _: JClass, webview_id: JString) -> jstring { + let Ok(webview_id) = env.get_string(&webview_id) else { + return env.new_string("wry.assets").unwrap().as_raw(); + }; + let webview_id = webview_id.to_str().ok().unwrap_or_default(); + if let Some(domain) = ASSET_LOADER_DOMAIN.lock().unwrap().get(webview_id) { env.new_string(domain).unwrap().as_raw() } else { env.new_string("wry.assets").unwrap().as_raw() @@ -383,33 +424,35 @@ pub unsafe fn assetLoaderDomain(env: JNIEnv, _: JClass) -> jstring { } #[allow(non_snake_case)] -pub unsafe fn onPageLoading(mut env: JNIEnv, _: JClass, url: JString) { - match env.get_string(&url) { - Ok(url) => { +pub unsafe fn onPageLoading(mut env: JNIEnv, _: JClass, webview_id: JString, url: JString) { + match (env.get_string(&url), env.get_string(&webview_id)) { + (Ok(url), Ok(webview_id)) => { let url = url.to_string_lossy().to_string(); - if let Some(on_load) = ON_LOAD_HANDLER.lock().unwrap().as_ref() { + let webview_id = webview_id.to_string_lossy().to_string(); + if let Some(on_load) = ON_LOAD_HANDLER.lock().unwrap().get(&webview_id) { (on_load.handler)(PageLoadEvent::Started, url) } } - Err(e) => { + (Err(_e), _) | (_, Err(_e)) => { #[cfg(feature = "tracing")] - tracing::warn!("Failed to parse JString: {}", e) + tracing::warn!("Failed to parse JString: {_e}") } } } #[allow(non_snake_case)] -pub unsafe fn onPageLoaded(mut env: JNIEnv, _: JClass, url: JString) { - match env.get_string(&url) { - Ok(url) => { +pub unsafe fn onPageLoaded(mut env: JNIEnv, _: JClass, webview_id: JString, url: JString) { + match (env.get_string(&url), env.get_string(&webview_id)) { + (Ok(url), Ok(webview_id)) => { let url = url.to_string_lossy().to_string(); - if let Some(on_load) = ON_LOAD_HANDLER.lock().unwrap().as_ref() { + let webview_id = webview_id.to_string_lossy().to_string(); + if let Some(on_load) = ON_LOAD_HANDLER.lock().unwrap().get(&webview_id) { (on_load.handler)(PageLoadEvent::Finished, url) } } - Err(e) => { + (Err(_e), _) | (_, Err(_e)) => { #[cfg(feature = "tracing")] - tracing::warn!("Failed to parse JString: {}", e) + tracing::warn!("Failed to parse JString: {_e}") } } } diff --git a/src/android/kotlin/Ipc.kt b/src/android/kotlin/Ipc.kt index e8f87612f..c76037506 100644 --- a/src/android/kotlin/Ipc.kt +++ b/src/android/kotlin/Ipc.kt @@ -8,24 +8,16 @@ package {{package}} import android.webkit.* -class Ipc(val webViewClient: RustWebViewClient) { +class Ipc(val webView: RustWebView, val webViewClient: RustWebViewClient) { @JavascriptInterface fun postMessage(message: String?) { message?.let {m -> // we're not using WebView::getUrl() here because it needs to be executed on the main thread // and it would slow down the Ipc // so instead we track the current URL on the webview client - this.ipc(webViewClient.currentUrl, m) + Rust.ipc(webView.id, webViewClient.currentUrl, m) } } - companion object { - init { - System.loadLibrary("{{library}}") - } - } - - private external fun ipc(url: String, message: String) - {{class-extension}} } diff --git a/src/android/kotlin/Rust.kt b/src/android/kotlin/Rust.kt new file mode 100644 index 000000000..85b2acbfe --- /dev/null +++ b/src/android/kotlin/Rust.kt @@ -0,0 +1,43 @@ +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +@file:Suppress("unused") + +package {{package}} + +import android.webkit.WebView +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse + +object Rust { + init { + System.loadLibrary("{{library}}") + } + + @JvmStatic external fun onActivityCreate(activity: WryActivity) + @JvmStatic external fun onActivityDestroy(activity: WryActivity) + @JvmStatic external fun onActivitySaveInstanceState() + @JvmStatic external fun onActivityLowMemory() + @JvmStatic external fun onWindowFocusChanged(activity: WryActivity, focus: Boolean) + + @JvmStatic external fun create() + @JvmStatic external fun start() + @JvmStatic external fun resume() + @JvmStatic external fun pause() + @JvmStatic external fun stop() + + @JvmStatic external fun onWebviewDestroy(activity: WryActivity, webviewId: String) + + @JvmStatic external fun ipc(webviewId: String, url: String, message: String) + + @JvmStatic external fun assetLoaderDomain(webviewId: String): String + @JvmStatic external fun withAssetLoader(webviewId: String): Boolean + @JvmStatic external fun handleRequest(webviewId: String, request: WebResourceRequest, isDocumentStartScriptEnabled: Boolean): WebResourceResponse? + @JvmStatic external fun shouldOverride(webviewId: String, url: String): Boolean + @JvmStatic external fun onPageLoading(webviewId: String, url: String) + @JvmStatic external fun onPageLoaded(webviewId: String, url: String) + @JvmStatic external fun onEval(webviewId: String, id: Int, result: String) + + @JvmStatic external fun handleReceivedTitle(webviewId: String, title: String) +} \ No newline at end of file diff --git a/src/android/kotlin/RustWebChromeClient.kt b/src/android/kotlin/RustWebChromeClient.kt index b89192602..bba26dbe2 100644 --- a/src/android/kotlin/RustWebChromeClient.kt +++ b/src/android/kotlin/RustWebChromeClient.kt @@ -486,8 +486,6 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { view: WebView, title: String ) { - handleReceivedTitle(view, title) + Rust.handleReceivedTitle((view as RustWebView).id, title) } - - private external fun handleReceivedTitle(webview: WebView, title: String) } diff --git a/src/android/kotlin/RustWebView.kt b/src/android/kotlin/RustWebView.kt index 8debc1c7a..31356c37a 100644 --- a/src/android/kotlin/RustWebView.kt +++ b/src/android/kotlin/RustWebView.kt @@ -50,13 +50,13 @@ class RustWebView(context: Context, val initScripts: Array, val id: Stri } override fun loadUrl(url: String) { - if (!shouldOverride(url)) { + if (!Rust.shouldOverride(id, url)) { super.loadUrl(url); } } override fun loadUrl(url: String, additionalHttpHeaders: Map) { - if (!shouldOverride(url)) { + if (!Rust.shouldOverride(id, url)) { super.loadUrl(url, additionalHttpHeaders); } } @@ -70,7 +70,7 @@ class RustWebView(context: Context, val initScripts: Array, val id: Stri fun evalScript(id: Int, script: String) { post { super.evaluateJavascript(script) { result -> - onEval(id, result) + Rust.onEval(this.id, id, result) } } } @@ -92,8 +92,5 @@ class RustWebView(context: Context, val initScripts: Array, val id: Stri return cookieManager.getCookie(url) } - private external fun shouldOverride(url: String): Boolean - private external fun onEval(id: Int, result: String) - {{class-extension}} } diff --git a/src/android/kotlin/RustWebViewClient.kt b/src/android/kotlin/RustWebViewClient.kt index 343ad1490..22bbaea75 100644 --- a/src/android/kotlin/RustWebViewClient.kt +++ b/src/android/kotlin/RustWebViewClient.kt @@ -12,14 +12,14 @@ import android.os.Handler import android.os.Looper import androidx.webkit.WebViewAssetLoader -class RustWebViewClient(context: Context): WebViewClient() { +class RustWebViewClient(webView: RustWebView, context: Context): WebViewClient() { private val interceptedState = mutableMapOf() var currentUrl: String = "about:blank" private var lastInterceptedUrl: Uri? = null private var pendingUrlRedirect: String? = null private val assetLoader = WebViewAssetLoader.Builder() - .setDomain(assetLoaderDomain()) + .setDomain(Rust.assetLoaderDomain(webView.id)) .addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(context)) .build() @@ -36,11 +36,11 @@ class RustWebViewClient(context: Context): WebViewClient() { } lastInterceptedUrl = request.url - return if (withAssetLoader()) { + return if (Rust.withAssetLoader((view as RustWebView).id)) { assetLoader.shouldInterceptRequest(request.url) } else { val rustWebview = view as RustWebView; - val response = handleRequest(rustWebview.id, request, rustWebview.isDocumentStartScriptEnabled) + val response = Rust.handleRequest(rustWebview.id, request, rustWebview.isDocumentStartScriptEnabled) interceptedState[request.url.toString()] = response != null return response } @@ -50,7 +50,7 @@ class RustWebViewClient(context: Context): WebViewClient() { view: WebView, request: WebResourceRequest ): Boolean { - return shouldOverride(request.url.toString()) + return Rust.shouldOverride((view as RustWebView).id, request.url.toString()) } override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { @@ -61,11 +61,11 @@ class RustWebViewClient(context: Context): WebViewClient() { view.evaluateJavascript(script, null) } } - return onPageLoading(url) + return Rust.onPageLoading((view as RustWebView).id, url) } override fun onPageFinished(view: WebView, url: String) { - onPageLoaded(url) + Rust.onPageLoaded((view as RustWebView).id, url) } override fun onReceivedError( @@ -88,18 +88,5 @@ class RustWebViewClient(context: Context): WebViewClient() { } } - companion object { - init { - System.loadLibrary("{{library}}") - } - } - - private external fun assetLoaderDomain(): String - private external fun withAssetLoader(): Boolean - private external fun handleRequest(webviewId: String, request: WebResourceRequest, isDocumentStartScriptEnabled: Boolean): WebResourceResponse? - private external fun shouldOverride(url: String): Boolean - private external fun onPageLoading(url: String) - private external fun onPageLoaded(url: String) - {{class-extension}} } diff --git a/src/android/kotlin/WryActivity.kt b/src/android/kotlin/WryActivity.kt index 44e5f27f0..0102d9577 100644 --- a/src/android/kotlin/WryActivity.kt +++ b/src/android/kotlin/WryActivity.kt @@ -4,16 +4,48 @@ package {{package}} -import {{package}}.RustWebView import android.annotation.SuppressLint import android.os.Build import android.os.Bundle import android.webkit.WebView import android.view.KeyEvent import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner + +private val ACTIVITY_ID_KEY = "__wryActivityId" + +object WryLifecycleObserver : DefaultLifecycleObserver { + override fun onCreate(owner: LifecycleOwner) { + super.onCreate(owner) + Rust.create() + } + + override fun onStart(owner: LifecycleOwner) { + super.onStart(owner) + Rust.start() + } + + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) + Rust.resume() + } + + override fun onPause(owner: LifecycleOwner) { + super.onPause(owner) + Rust.pause() + } + + override fun onStop(owner: LifecycleOwner) { + super.onStop(owner) + Rust.stop() + } +} abstract class WryActivity : AppCompatActivity() { private lateinit var mWebView: RustWebView + var id: Int = 0 open val handleBackNavigation: Boolean = true open fun onWebViewCreate(webView: WebView) { } @@ -58,52 +90,35 @@ abstract class WryActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - create(this) - } - - override fun onStart() { - super.onStart() - start() - } - - override fun onResume() { - super.onResume() - resume() - } - - override fun onPause() { - super.onPause() - pause() - } - - override fun onStop() { - super.onStop() - stop() + id = savedInstanceState?.getInt(ACTIVITY_ID_KEY) ?: hashCode() + ProcessLifecycleOwner.get().lifecycle.addObserver(WryLifecycleObserver) + Rust.onActivityCreate(this) } override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) - focus(hasFocus) + Rust.onWindowFocusChanged(this, hasFocus) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - save() + outState.putInt(ACTIVITY_ID_KEY, id) + Rust.onActivitySaveInstanceState() } override fun onDestroy() { super.onDestroy() - destroy() - onActivityDestroy() + Rust.onActivityDestroy(this) + Rust.onWebviewDestroy(this, if (::mWebView.isInitialized) { mWebView.id } else { "" }) } override fun onLowMemory() { super.onLowMemory() - memory() + Rust.onActivityLowMemory() } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { - if (handleBackNavigation && keyCode == KeyEvent.KEYCODE_BACK && mWebView.canGoBack()) { + if (handleBackNavigation && keyCode == KeyEvent.KEYCODE_BACK && if (::mWebView.isInitialized) { mWebView.canGoBack() } else { false }) { mWebView.goBack() return true } @@ -114,22 +129,5 @@ abstract class WryActivity : AppCompatActivity() { return Class.forName(name) } - companion object { - init { - System.loadLibrary("{{library}}") - } - } - - private external fun create(activity: WryActivity) - private external fun start() - private external fun resume() - private external fun pause() - private external fun stop() - private external fun save() - private external fun destroy() - private external fun onActivityDestroy() - private external fun memory() - private external fun focus(focus: Boolean) - {{class-extension}} } diff --git a/src/android/main_pipe.rs b/src/android/main_pipe.rs index 5b81048ef..82c526bb8 100644 --- a/src/android/main_pipe.rs +++ b/src/android/main_pipe.rs @@ -9,17 +9,85 @@ use jni::{ objects::{GlobalRef, JMap, JObject, JString}, JNIEnv, }; -use once_cell::sync::Lazy; -use std::os::unix::prelude::*; +use std::{ + collections::BTreeMap, + os::unix::prelude::*, + sync::{Arc, Mutex}, +}; + +use super::{find_class, EvalCallback, WebviewId, EVAL_CALLBACKS, EVAL_ID_GENERATOR, PACKAGE}; + +pub type ActivityId = i32; + +#[derive(Clone)] +pub struct ActivityProxy { + pub channel: (Sender, Receiver), + pub pipe: Arc<[OwnedFd; 2]>, + pub activity: GlobalRef, + pub webview: Option, + pub webchrome_client: GlobalRef, +} + +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); + Self { + channel, + pipe: Arc::new(pipe), + activity, + webview: None, + webchrome_client, + } + } +} + +lazy_static::lazy_static! { + static ref ACTIVITY_PROXY: Mutex> = + Mutex::new(BTreeMap::new()); +} -use super::{find_class, EvalCallback, EVAL_CALLBACKS, EVAL_ID_GENERATOR, PACKAGE}; +pub fn activity_proxy(id: ActivityId) -> Option { + ACTIVITY_PROXY.lock().unwrap().get(&id).cloned() +} + +pub fn register_activity_proxy( + id: ActivityId, + activity: 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.webchrome_client = webchrome_client; + proxy.clone() + } else { + let proxy = ActivityProxy::new(activity, webchrome_client); + activity_proxy.insert(id, proxy.clone()); + proxy + } +} + +pub fn last_activity_id() -> Option { + ACTIVITY_PROXY.lock().unwrap().keys().next_back().cloned() +} -static CHANNEL: Lazy<(Sender, Receiver)> = 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)) } -}); +pub fn first_activity_id() -> Option { + ACTIVITY_PROXY.lock().unwrap().keys().next().cloned() +} + +pub fn get_webview(activity_id: ActivityId) -> Option { + ACTIVITY_PROXY + .lock() + .unwrap() + .get(&activity_id) + .unwrap() + .webview + .as_ref() + .cloned() +} pub enum MainPipeState { Alive, @@ -28,18 +96,21 @@ pub enum MainPipeState { pub struct MainPipe<'a> { pub env: JNIEnv<'a>, - pub activity: GlobalRef, - pub webview: Option, - pub webchrome_client: GlobalRef, + pub activity_id: ActivityId, } impl<'a> MainPipe<'a> { - pub(crate) fn send(message: WebViewMessage) { + pub(crate) fn send(activity_id: ActivityId, message: WebViewMessage) { let size = std::mem::size_of::(); - if let Ok(()) = CHANNEL.0.send(message) { + 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) { unsafe { libc::write( - MAIN_PIPE[1].as_raw_fd(), + proxy.pipe[1].as_raw_fd(), &true as *const _ as *const _, size, ) @@ -48,10 +119,25 @@ impl<'a> MainPipe<'a> { } pub fn recv(&mut self) -> JniResult { - let activity = self.activity.as_obj(); - if let Ok(message) = CHANNEL.1.recv() { + 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() { 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())) + else { + #[cfg(debug_assertions)] + eprintln!("no activity found for activity id: {}", self.activity_id); + return Ok(MainPipeState::Destroyed); + }; let CreateWebViewAttributes { url, html, @@ -82,25 +168,22 @@ impl<'a> MainPipe<'a> { self.env.new_string(init_script.script)?, )?; } - let id = self.env.new_string(id)?; - // Create webview let rust_webview_class = find_class( &mut self.env, - activity, + &activity, format!("{}/RustWebView", PACKAGE.get().unwrap()), )?; let webview = self.env.new_object( &rust_webview_class, "(Landroid/content/Context;[Ljava/lang/String;Ljava/lang/String;)V", &[ - activity.into(), + (&activity).into(), (&initialization_scripts_array).into(), (&id).into(), ], )?; - // get settings let web_settings = self .env @@ -111,7 +194,6 @@ impl<'a> MainPipe<'a> { &[], )? .l()?; - // set media autoplay self.env.call_method( &web_settings, @@ -119,7 +201,6 @@ impl<'a> MainPipe<'a> { "(Z)V", &[(!autoplay).into()], )?; - // set user-agent if let Some(user_agent) = user_agent { let user_agent = self.env.new_string(user_agent)?; @@ -141,13 +222,13 @@ impl<'a> MainPipe<'a> { )?; } + let webview_class_name = format!("{}/RustWebView", PACKAGE.get().unwrap()); self.env.call_method( - activity, + &activity, "setWebView", - format!("(L{}/RustWebView;)V", PACKAGE.get().unwrap()), + format!("(L{webview_class_name};)V"), &[(&webview).into()], )?; - // Navigation if let Some(u) = url { if let Ok(url) = self.env.new_string(u) { @@ -158,7 +239,6 @@ impl<'a> MainPipe<'a> { load_html(&mut self.env, &webview, &html)?; } } - // Enable devtools #[cfg(any(debug_assertions, feature = "devtools"))] self.env.call_static_method( @@ -167,21 +247,19 @@ impl<'a> MainPipe<'a> { "(Z)V", &[devtools.into()], )?; - if transparent { set_background_color(&mut self.env, &webview, (0, 0, 0, 0))?; } else if let Some(color) = background_color { set_background_color(&mut self.env, &webview, color)?; } - // Create and set webview client let client_class_name = format!("{}/RustWebViewClient", PACKAGE.get().unwrap()); let rust_webview_client_class = - find_class(&mut self.env, activity, client_class_name.clone())?; + find_class(&mut self.env, &activity, client_class_name.clone())?; let webview_client = self.env.new_object( &rust_webview_client_class, - "(Landroid/content/Context;)V", - &[activity.into()], + format!("(L{webview_class_name};Landroid/content/Context;)V"), + &[(&webview).into(), (&activity).into()], )?; self.env.call_method( &webview, @@ -189,25 +267,24 @@ impl<'a> MainPipe<'a> { "(Landroid/webkit/WebViewClient;)V", &[(&webview_client).into()], )?; - // set webchrome client self.env.call_method( &webview, "setWebChromeClient", "(Landroid/webkit/WebChromeClient;)V", - &[self.webchrome_client.as_obj().into()], + &[web_chrome_client.as_obj().into()], )?; // Add javascript interface (IPC) let ipc_class = find_class( &mut self.env, - activity, + &activity, format!("{}/Ipc", PACKAGE.get().unwrap()), )?; let ipc = self.env.new_object( ipc_class, - format!("(L{client_class_name};)V"), - &[(&webview_client).into()], + format!("(L{webview_class_name};L{client_class_name};)V"), + &[(&webview).into(), (&webview_client).into()], )?; let ipc_str = self.env.new_string("ipc")?; self.env.call_method( @@ -219,29 +296,35 @@ impl<'a> MainPipe<'a> { // Set content view self.env.call_method( - activity, + &activity, "setContentView", "(Landroid/view/View;)V", &[(&webview).into()], )?; if let Some(on_webview_created) = on_webview_created { - if let Err(e) = on_webview_created(super::Context { + if let Err(_e) = on_webview_created(super::Context { env: &mut self.env, - activity, + activity: &activity, webview: &webview, }) { #[cfg(feature = "tracing")] - tracing::warn!("failed to run webview created hook: {e}"); + tracing::warn!("failed to run webview created hook: {_e}"); } } let webview = self.env.new_global_ref(webview)?; - self.webview = Some(webview); + ACTIVITY_PROXY + .lock() + .unwrap() + .get_mut(&self.activity_id) + .unwrap() + .webview + .replace(webview); } WebViewMessage::Eval(script, callback) => { - if let Some(webview) = &self.webview { + if let Some(webview) = get_webview(self.activity_id) { let id = EVAL_ID_GENERATOR.next() as i32; #[cfg(feature = "tracing")] @@ -275,30 +358,34 @@ impl<'a> MainPipe<'a> { } } WebViewMessage::SetBackgroundColor(background_color) => { - if let Some(webview) = &self.webview { + if let Some(webview) = get_webview(self.activity_id) { set_background_color(&mut self.env, webview.as_obj(), background_color)?; } } WebViewMessage::GetWebViewVersion(tx) => { - match self - .env - .call_method(activity, "getVersion", "()Ljava/lang/String;", &[]) - .and_then(|v| v.l()) - .and_then(|s| { - let s = JString::from(s); - self - .env - .get_string(&s) - .map(|v| v.to_string_lossy().to_string()) - }) { - Ok(version) => { - tx.send(Ok(version)).unwrap(); + if let Some(activity) = activity_proxy(self.activity_id).map(|p| p.activity.clone()) { + match self + .env + .call_method(activity, "getVersion", "()Ljava/lang/String;", &[]) + .and_then(|v| v.l()) + .and_then(|s| { + let s = JString::from(s); + self + .env + .get_string(&s) + .map(|v| v.to_string_lossy().to_string()) + }) { + Ok(version) => { + tx.send(Ok(version)).unwrap(); + } + Err(e) => tx.send(Err(e.into())).unwrap(), } - Err(e) => tx.send(Err(e.into())).unwrap(), + } else { + tx.send(Err(Error::ActivityNotFound)).unwrap(); } } WebViewMessage::GetUrl(tx) => { - if let Some(webview) = &self.webview { + if let Some(webview) = get_webview(self.activity_id) { let url = self .env .call_method(webview.as_obj(), "getUrl", "()Ljava/lang/String;", &[]) @@ -316,38 +403,44 @@ impl<'a> MainPipe<'a> { } } WebViewMessage::Jni(f) => { - if let Some(w) = &self.webview { - f(&mut self.env, activity, w.as_obj()); - } else { - f(&mut self.env, activity, &JObject::null()); + match activity_proxy(self.activity_id).map(|p| (p.activity.clone(), p.webview.clone())) { + Some((activity, Some(webview))) => { + f(&mut self.env, &activity, webview.as_obj()); + } + Some((activity, None)) => { + f(&mut self.env, &activity, &JObject::null()); + } + _ => { + f(&mut self.env, &JObject::null(), &JObject::null()); + } } } WebViewMessage::LoadUrl(url, headers) => { - if let Some(webview) = &self.webview { + if let Some(webview) = get_webview(self.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) = &self.webview { + if let Some(webview) = get_webview(self.activity_id) { self .env .call_method(webview, "clearAllBrowsingData", "()V", &[])?; } } WebViewMessage::LoadHtml(html) => { - if let Some(webview) = &self.webview { + if let Some(webview) = get_webview(self.activity_id) { let html = self.env.new_string(html)?; load_html(&mut self.env, webview.as_obj(), &html)?; } } WebViewMessage::Reload => { - if let Some(webview) = &self.webview { + if let Some(webview) = get_webview(self.activity_id) { reload(&mut self.env, webview.as_obj())?; } } WebViewMessage::GetCookies(tx, url) => { - if let Some(webview) = &self.webview { + if let Some(webview) = get_webview(self.activity_id) { let url = self.env.new_string(url)?; let cookies = self .env @@ -376,8 +469,17 @@ impl<'a> MainPipe<'a> { .unwrap(); } } - WebViewMessage::OnDestroy => { - return Ok(MainPipeState::Destroyed); + WebViewMessage::OnDestroy { + activity_id, + webview_id, + is_changing_configurations, + } => { + // keep our webview references (callbacks etc) alive if the activity is going to be recreated due to configuration changes + // e.g. rotation, multi-window mode change, etc + if !is_changing_configurations { + super::destroy_webview(activity_id, &webview_id); + return Ok(MainPipeState::Destroyed); + } } } } @@ -457,9 +559,14 @@ pub(crate) enum WebViewMessage { LoadHtml(String), Reload, ClearAllBrowsingData, - OnDestroy, + OnDestroy { + activity_id: ActivityId, + webview_id: WebviewId, + is_changing_configurations: bool, + }, } +#[derive(Clone)] pub(crate) struct CreateWebViewAttributes { pub id: String, pub url: Option, @@ -470,7 +577,8 @@ pub(crate) struct CreateWebViewAttributes { pub background_color: Option, pub headers: Option, pub autoplay: bool, - pub on_webview_created: Option JniResult<()> + Send>>, + pub on_webview_created: + Option JniResult<()> + Send + Sync + 'static>>, pub user_agent: Option, pub initialization_scripts: Vec, pub javascript_disabled: bool, diff --git a/src/android/mod.rs b/src/android/mod.rs index 6f6805e0e..4925ec2b4 100644 --- a/src/android/mod.rs +++ b/src/android/mod.rs @@ -31,7 +31,10 @@ use std::{ pub(crate) mod binding; mod main_pipe; -use main_pipe::{CreateWebViewAttributes, MainPipe, MainPipeState, WebViewMessage, MAIN_PIPE}; +use main_pipe::{ + first_activity_id, last_activity_id, register_activity_proxy, ActivityId, + CreateWebViewAttributes, MainPipe, MainPipeState, WebViewMessage, +}; use crate::util::Counter; @@ -44,22 +47,19 @@ pub struct Context<'a, 'b> { pub webview: &'a JObject<'b>, } -pub(crate) struct StaticValue(Mutex); - -unsafe impl Send for StaticValue {} -unsafe impl Sync for StaticValue {} - -impl std::ops::Deref for StaticValue { - type Target = Mutex; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} +type WebviewId = String; macro_rules! define_static_handlers { + ($($key: ident, $var:ident = $type_name:ident);+ $(;)?) => { + $(lazy_static::lazy_static! { + static ref $var: Mutex> = Mutex::new(HashMap::new()); + })* + }; + ($($var:ident = $type_name:ident { $($fields:ident:$types:ty),+ $(,)? });+ $(;)?) => { - $(pub static $var: StaticValue> = StaticValue(Mutex::new(None)); + $(lazy_static::lazy_static! { + static ref $var: Mutex> = Mutex::new(HashMap::new()); + } pub struct $type_name { $($fields: $types,)* } @@ -76,15 +76,17 @@ macro_rules! define_static_handlers { } define_static_handlers! { - IPC = UnsafeIpc { handler: Box)> }; + IPC = UnsafeIpc { handler: Box)> }; REQUEST_HANDLER = UnsafeRequestHandler { handler: Box>, bool) -> Option>>> }; TITLE_CHANGE_HANDLER = UnsafeTitleHandler { handler: Box }; URL_LOADING_OVERRIDE = UnsafeUrlLoadingOverride { handler: Box bool> }; ON_LOAD_HANDLER = UnsafeOnPageLoadHandler { handler: Box }; } - -pub static WITH_ASSET_LOADER: StaticValue> = StaticValue(Mutex::new(None)); -pub static ASSET_LOADER_DOMAIN: StaticValue> = StaticValue(Mutex::new(None)); +define_static_handlers! { + WebviewId, WITH_ASSET_LOADER = bool; + WebviewId, ASSET_LOADER_DOMAIN = String; + ActivityId, WEBVIEW_ATTRIBUTES = CreateWebViewAttributes; +} pub(crate) static PACKAGE: OnceCell = OnceCell::new(); @@ -93,6 +95,17 @@ type EvalCallback = Box; pub static EVAL_ID_GENERATOR: Counter = Counter::new(); pub static EVAL_CALLBACKS: OnceCell>> = OnceCell::new(); +pub fn destroy_webview(activity_id: ActivityId, webview_id: &WebviewId) { + WEBVIEW_ATTRIBUTES.lock().unwrap().remove(&activity_id); + IPC.lock().unwrap().remove(webview_id); + REQUEST_HANDLER.lock().unwrap().remove(webview_id); + TITLE_CHANGE_HANDLER.lock().unwrap().remove(webview_id); + URL_LOADING_OVERRIDE.lock().unwrap().remove(webview_id); + ON_LOAD_HANDLER.lock().unwrap().remove(webview_id); + WITH_ASSET_LOADER.lock().unwrap().remove(webview_id); + ASSET_LOADER_DOMAIN.lock().unwrap().remove(webview_id); +} + /// Sets up the necessary logic for wry to be able to create the webviews later. /// /// This function must be run on the thread where the [`JNIEnv`] is registered and the looper is local, @@ -105,6 +118,12 @@ pub unsafe fn android_setup( ) { PACKAGE.get_or_init(move || package.to_string()); + let activity_id = env + .call_method(activity.as_obj(), "getId", "()I", &[]) + .unwrap() + .i() + .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( @@ -116,37 +135,49 @@ pub unsafe fn android_setup( let webchrome_client = env .new_object( &rust_webchrome_client_class, - &format!("(L{}/WryActivity;)V", PACKAGE.get().unwrap()), + format!("(L{}/WryActivity;)V", PACKAGE.get().unwrap()), &[activity.as_obj().into()], ) .unwrap(); let webchrome_client = env.new_global_ref(webchrome_client).unwrap(); - let mut main_pipe = MainPipe { - env, - activity, - webview: None, - webchrome_client, - }; - looper - .add_fd_with_callback(MAIN_PIPE[0].as_fd(), FdEvent::INPUT, move |fd, _event| { - let size = std::mem::size_of::(); - 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(); + let activity_proxy = register_activity_proxy(activity_id, activity, 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::(); + 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(); + } } pub(crate) struct InnerWebView { id: String, + pub activity_id: ActivityId, } impl InnerWebView { @@ -163,6 +194,7 @@ impl InnerWebView { attributes: WebViewAttributes, pl_attrs: super::PlatformSpecificWebViewAttributes, ) -> Result { + let activity_id = last_activity_id().expect("no available activity"); let WebViewAttributes { url, html, @@ -208,123 +240,133 @@ impl InnerWebView { .map(|id| id.to_string()) .unwrap_or_else(|| COUNTER.next().to_string()); - WITH_ASSET_LOADER.lock().unwrap().replace(with_asset_loader); + WITH_ASSET_LOADER + .lock() + .unwrap() + .insert(id.clone(), with_asset_loader); if let Some(domain) = asset_loader_domain { - ASSET_LOADER_DOMAIN.lock().unwrap().replace(domain); + ASSET_LOADER_DOMAIN + .lock() + .unwrap() + .insert(id.clone(), domain); } let initialization_scripts_ = initialization_scripts.clone(); - REQUEST_HANDLER.lock() - .unwrap().replace( - UnsafeRequestHandler::new(Box::new( - move |webview_id: &str, mut request, is_document_start_script_enabled| { - let uri = request.uri().to_string(); - if let Some((custom_protocol_uri, custom_protocol_closure)) = custom_protocols.iter().find(|(name, _)| { - uri.starts_with(&format!("{scheme}://{}.", name)) - }) { - let uri_res = uri - .replace( - &format!("{scheme}://{}.", custom_protocol_uri), - &format!("{}://", custom_protocol_uri), - ) - .parse(); - - if let Ok(uri) = uri_res { - *request.uri_mut() = uri; - } - - let (tx, rx) = channel(); - let initialization_scripts = initialization_scripts_.clone(); - let responder: Box>)> = - Box::new(move |mut response| { - if !is_document_start_script_enabled { - #[cfg(feature = "tracing")] - tracing::info!("`addDocumentStartJavaScript` is not supported; injecting initialization scripts via custom protocol handler"); - let should_inject_scripts = response - .headers() - .get(CONTENT_TYPE) - // Content-Type must begin with the media type, but is case-insensitive. - // It may also be followed by any number of semicolon-delimited key value pairs. - // We don't care about these here. - // source: https://httpwg.org/specs/rfc9110.html#rfc.section.8.3.1 - .and_then(|content_type| content_type.to_str().ok()) - .map(|content_type_str| { - content_type_str.to_lowercase().starts_with("text/html") - }) - .unwrap_or_default(); - - if should_inject_scripts && !initialization_scripts.is_empty() { - let mut document = kuchiki::parse_html() - .one(String::from_utf8_lossy(response.body()).as_ref()).document_node; - let csp = response.headers_mut().get_mut(CONTENT_SECURITY_POLICY); - let mut hashes = Vec::new(); - with_html_head(&mut document, |head| { - // iterate in reverse order since we are prepending each script to the head tag - for init_script in initialization_scripts.iter().rev() { - let script_el = NodeRef::new_element( - QualName::new(None, ns!(html), "script".into()), - None, - ); - script_el.append(NodeRef::new_text(init_script.script.as_str())); - head.prepend(script_el); - if csp.is_some() { - hashes.push(hash_script(init_script.script.as_str())); + REQUEST_HANDLER.lock().unwrap() + .insert( + id.clone(), + UnsafeRequestHandler::new(Box::new( + move |webview_id: &str, mut request, is_document_start_script_enabled| { + let uri = request.uri().to_string(); + if let Some((custom_protocol_uri, custom_protocol_closure)) = custom_protocols.iter().find(|(name, _)| { + uri.starts_with(&format!("{scheme}://{}.", name)) + }) { + let uri_res = uri + .replace( + &format!("{scheme}://{}.", custom_protocol_uri), + &format!("{}://", custom_protocol_uri), + ) + .parse(); + + if let Ok(uri) = uri_res { + *request.uri_mut() = uri; + } + + let (tx, rx) = channel(); + let initialization_scripts = initialization_scripts_.clone(); + let responder: Box>)> = + Box::new(move |mut response| { + if !is_document_start_script_enabled { + #[cfg(feature = "tracing")] + tracing::info!("`addDocumentStartJavaScript` is not supported; injecting initialization scripts via custom protocol handler"); + let should_inject_scripts = response + .headers() + .get(CONTENT_TYPE) + // Content-Type must begin with the media type, but is case-insensitive. + // It may also be followed by any number of semicolon-delimited key value pairs. + // We don't care about these here. + // source: https://httpwg.org/specs/rfc9110.html#rfc.section.8.3.1 + .and_then(|content_type| content_type.to_str().ok()) + .map(|content_type_str| { + content_type_str.to_lowercase().starts_with("text/html") + }) + .unwrap_or_default(); + + if should_inject_scripts && !initialization_scripts.is_empty() { + let mut document = kuchiki::parse_html() + .one(String::from_utf8_lossy(response.body()).as_ref()).document_node; + let csp = response.headers_mut().get_mut(CONTENT_SECURITY_POLICY); + let mut hashes = Vec::new(); + with_html_head(&mut document, |head| { + // iterate in reverse order since we are prepending each script to the head tag + for init_script in initialization_scripts.iter().rev() { + let script_el = NodeRef::new_element( + QualName::new(None, ns!(html), "script".into()), + None, + ); + script_el.append(NodeRef::new_text(init_script.script.as_str())); + head.prepend(script_el); + if csp.is_some() { + hashes.push(hash_script(init_script.script.as_str())); + } } + }); + + if let Some(csp) = csp { + let csp_string = csp.to_str().unwrap().to_string(); + let csp_string = if csp_string.contains("script-src") { + csp_string + .replace("script-src", &format!("script-src {}", hashes.join(" "))) + } else { + format!("{} script-src {}", csp_string, hashes.join(" ")) + }; + *csp = HeaderValue::from_str(&csp_string).unwrap(); } - }); - - if let Some(csp) = csp { - let csp_string = csp.to_str().unwrap().to_string(); - let csp_string = if csp_string.contains("script-src") { - csp_string - .replace("script-src", &format!("script-src {}", hashes.join(" "))) - } else { - format!("{} script-src {}", csp_string, hashes.join(" ")) - }; - *csp = HeaderValue::from_str(&csp_string).unwrap(); - } - *response.body_mut() = document.to_string().into_bytes().into(); + *response.body_mut() = document.to_string().into_bytes().into(); + } } - } - tx.send(response).unwrap(); - }); + tx.send(response).unwrap(); + }); - (custom_protocol_closure)(webview_id, request, RequestAsyncResponder { responder }); - return Some(rx.recv_timeout(MAIN_PIPE_TIMEOUT).unwrap()); - } - None - }, - ) - )); + (custom_protocol_closure)(webview_id, request, RequestAsyncResponder { responder }); + return Some(rx.recv_timeout(MAIN_PIPE_TIMEOUT).unwrap()); + } + None + }, + ) + )); if let Some(i) = ipc_handler { - IPC.lock().unwrap().replace(UnsafeIpc::new(Box::new(i))); + IPC + .lock() + .unwrap() + .insert(id.clone(), UnsafeIpc::new(Box::new(i))); } if let Some(i) = attributes.document_title_changed_handler { TITLE_CHANGE_HANDLER .lock() .unwrap() - .replace(UnsafeTitleHandler::new(i)); + .insert(id.clone(), UnsafeTitleHandler::new(i)); } if let Some(i) = attributes.navigation_handler { URL_LOADING_OVERRIDE .lock() .unwrap() - .replace(UnsafeUrlLoadingOverride::new(i)); + .insert(id.clone(), UnsafeUrlLoadingOverride::new(i)); } if let Some(h) = attributes.on_page_load_handler { ON_LOAD_HANDLER .lock() .unwrap() - .replace(UnsafeOnPageLoadHandler::new(h)); + .insert(id.clone(), UnsafeOnPageLoadHandler::new(h)); } - MainPipe::send(WebViewMessage::CreateWebView(CreateWebViewAttributes { + let attributes = CreateWebViewAttributes { id: id.clone(), url, html, @@ -338,30 +380,40 @@ impl InnerWebView { user_agent, initialization_scripts, javascript_disabled, - })); + }; + + WEBVIEW_ATTRIBUTES + .lock() + .unwrap() + .insert(activity_id, attributes.clone()); + + MainPipe::send(activity_id, WebViewMessage::CreateWebView(attributes)); - Ok(Self { id }) + Ok(Self { id, activity_id }) } pub fn print(&self) -> crate::Result<()> { Ok(()) } - pub fn id(&self) -> crate::WebViewId { + pub fn id(&self) -> crate::WebViewId<'_> { &self.id } pub fn url(&self) -> crate::Result { let (tx, rx) = bounded(1); - MainPipe::send(WebViewMessage::GetUrl(tx)); + MainPipe::send(self.activity_id, WebViewMessage::GetUrl(tx)); rx.recv_timeout(MAIN_PIPE_TIMEOUT).map_err(Into::into) } pub fn eval(&self, js: &str, callback: Option) -> Result<()> { - MainPipe::send(WebViewMessage::Eval( - js.into(), - callback.map(|c| Box::new(c) as Box), - )); + MainPipe::send( + self.activity_id, + WebViewMessage::Eval( + js.into(), + callback.map(|c| Box::new(c) as Box), + ), + ); Ok(()) } @@ -381,47 +433,59 @@ impl InnerWebView { } pub fn set_background_color(&self, background_color: RGBA) -> Result<()> { - MainPipe::send(WebViewMessage::SetBackgroundColor(background_color)); + MainPipe::send( + self.activity_id, + WebViewMessage::SetBackgroundColor(background_color), + ); Ok(()) } pub fn load_url(&self, url: &str) -> Result<()> { - MainPipe::send(WebViewMessage::LoadUrl(url.to_string(), None)); + MainPipe::send( + self.activity_id, + WebViewMessage::LoadUrl(url.to_string(), None), + ); Ok(()) } pub fn load_url_with_headers(&self, url: &str, headers: http::HeaderMap) -> Result<()> { - MainPipe::send(WebViewMessage::LoadUrl(url.to_string(), Some(headers))); + MainPipe::send( + self.activity_id, + WebViewMessage::LoadUrl(url.to_string(), Some(headers)), + ); Ok(()) } pub fn load_html(&self, html: &str) -> Result<()> { - MainPipe::send(WebViewMessage::LoadHtml(html.to_string())); + MainPipe::send(self.activity_id, WebViewMessage::LoadHtml(html.to_string())); Ok(()) } pub fn reload(&self) -> Result<()> { - MainPipe::send(WebViewMessage::Reload); + MainPipe::send(self.activity_id, WebViewMessage::Reload); Ok(()) } pub fn clear_all_browsing_data(&self) -> Result<()> { - MainPipe::send(WebViewMessage::ClearAllBrowsingData); + MainPipe::send(self.activity_id, WebViewMessage::ClearAllBrowsingData); Ok(()) } pub fn cookies_for_url(&self, url: &str) -> Result>> { let (tx, rx) = bounded(1); - MainPipe::send(WebViewMessage::GetCookies(tx, url.to_string())); + MainPipe::send( + self.activity_id, + WebViewMessage::GetCookies(tx, url.to_string()), + ); rx.recv_timeout(MAIN_PIPE_TIMEOUT).map_err(Into::into) } - pub fn set_cookie(&self, cookie: &cookie::Cookie<'_>) -> Result<()> { + pub fn set_cookie(&self, _cookie: &cookie::Cookie<'_>) -> Result<()> { // Unsupported Ok(()) } - pub fn delete_cookie(&self, cookie: &cookie::Cookie<'_>) -> Result<()> { + pub fn delete_cookie(&self, _cookie: &cookie::Cookie<'_>) -> Result<()> { // Unsupported Ok(()) } @@ -456,7 +520,9 @@ impl InnerWebView { } #[derive(Clone, Copy)] -pub struct JniHandle; +pub struct JniHandle { + pub(crate) activity_id: ActivityId, +} impl JniHandle { /// Execute jni code on the thread of the webview. @@ -465,13 +531,21 @@ impl JniHandle { where F: FnOnce(&mut JNIEnv, &JObject, &JObject) + Send + 'static, { - MainPipe::send(WebViewMessage::Jni(Box::new(func))); + MainPipe::send(self.activity_id, WebViewMessage::Jni(Box::new(func))); } } pub fn platform_webview_version() -> Result { let (tx, rx) = bounded(1); - MainPipe::send(WebViewMessage::GetWebViewVersion(tx)); + let activity_id = loop { + match first_activity_id() { + Some(id) => break id, + None => { + std::thread::sleep(Duration::from_millis(100)); + } + } + }; + MainPipe::send(activity_id, WebViewMessage::GetWebViewVersion(tx)); rx.recv_timeout(MAIN_PIPE_TIMEOUT).unwrap() } @@ -520,5 +594,8 @@ pub fn dispatch(func: F) where F: FnOnce(&mut JNIEnv, &JObject, &JObject) + Send + 'static, { - MainPipe::send(WebViewMessage::Jni(Box::new(func))); + MainPipe::send( + first_activity_id().expect("no available activity"), + WebViewMessage::Jni(Box::new(func)), + ); } diff --git a/src/error.rs b/src/error.rs index aed324ee4..959899311 100644 --- a/src/error.rs +++ b/src/error.rs @@ -74,4 +74,7 @@ pub enum Error { #[cfg(any(target_os = "macos", target_os = "ios"))] #[error("data store is currently opened")] DataStoreInUse, + #[cfg(target_os = "android")] + #[error("Activity not found")] + ActivityNotFound, } diff --git a/src/lib.rs b/src/lib.rs index 2b2706ec7..8ad14458f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1782,8 +1782,14 @@ impl WebViewBuilderExtWindows for WebViewBuilder<'_> { #[cfg(target_os = "android")] #[derive(Default)] pub(crate) struct PlatformSpecificWebViewAttributes { - on_webview_created: - Option std::result::Result<(), jni::errors::Error> + Send>>, + on_webview_created: Option< + std::sync::Arc< + dyn Fn(prelude::Context) -> std::result::Result<(), jni::errors::Error> + + Send + + Sync + + 'static, + >, + >, with_asset_loader: bool, asset_loader_domain: Option, https_scheme: bool, @@ -1792,7 +1798,10 @@ pub(crate) struct PlatformSpecificWebViewAttributes { #[cfg(target_os = "android")] pub trait WebViewBuilderExtAndroid { fn on_webview_created< - F: Fn(prelude::Context<'_, '_>) -> std::result::Result<(), jni::errors::Error> + Send + 'static, + F: Fn(prelude::Context<'_, '_>) -> std::result::Result<(), jni::errors::Error> + + Send + + Sync + + 'static, >( self, f: F, @@ -1819,12 +1828,15 @@ pub trait WebViewBuilderExtAndroid { #[cfg(target_os = "android")] impl WebViewBuilderExtAndroid for WebViewBuilder<'_> { fn on_webview_created< - F: Fn(prelude::Context<'_, '_>) -> std::result::Result<(), jni::errors::Error> + Send + 'static, + F: Fn(prelude::Context<'_, '_>) -> std::result::Result<(), jni::errors::Error> + + Send + + Sync + + 'static, >( mut self, f: F, ) -> Self { - self.platform_specific.on_webview_created = Some(Box::new(f)); + self.platform_specific.on_webview_created = Some(std::sync::Arc::new(f)); self } @@ -2431,7 +2443,9 @@ pub trait WebViewExtAndroid { #[cfg(target_os = "android")] impl WebViewExtAndroid for WebView { fn handle(&self) -> JniHandle { - JniHandle + JniHandle { + activity_id: self.webview.activity_id, + } } } From df04aa87292ddccdda03bc97b32c72ceba8db099 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Thu, 23 Oct 2025 15:27:28 -0300 Subject: [PATCH 2/9] properly select activity, fix messaging for multi webview on android --- src/android/binding.rs | 25 +++++ src/android/kotlin/Rust.kt | 1 + src/android/kotlin/RustWebViewClient.kt | 15 +-- src/android/kotlin/WryActivity.kt | 12 ++- src/android/kotlin/proguard-wry.pro | 1 + src/android/main_pipe.rs | 126 +++++++++++++----------- src/android/mod.rs | 66 ++++++------- 7 files changed, 147 insertions(+), 99 deletions(-) diff --git a/src/android/binding.rs b/src/android/binding.rs index fd1489320..031cd09cc 100644 --- a/src/android/binding.rs +++ b/src/android/binding.rs @@ -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::(); + 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 diff --git a/src/android/kotlin/Rust.kt b/src/android/kotlin/Rust.kt index 85b2acbfe..b3126bb1c 100644 --- a/src/android/kotlin/Rust.kt +++ b/src/android/kotlin/Rust.kt @@ -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) diff --git a/src/android/kotlin/RustWebViewClient.kt b/src/android/kotlin/RustWebViewClient.kt index 22bbaea75..43c63adc4 100644 --- a/src/android/kotlin/RustWebViewClient.kt +++ b/src/android/kotlin/RustWebViewClient.kt @@ -15,7 +15,6 @@ import androidx.webkit.WebViewAssetLoader class RustWebViewClient(webView: RustWebView, context: Context): WebViewClient() { private val interceptedState = mutableMapOf() 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) } diff --git a/src/android/kotlin/WryActivity.kt b/src/android/kotlin/WryActivity.kt index 0102d9577..00ae0d933 100644 --- a/src/android/kotlin/WryActivity.kt +++ b/src/android/kotlin/WryActivity.kt @@ -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}} } diff --git a/src/android/kotlin/proguard-wry.pro b/src/android/kotlin/proguard-wry.pro index 064ca362b..5cd5a1413 100644 --- a/src/android/kotlin/proguard-wry.pro +++ b/src/android/kotlin/proguard-wry.pro @@ -12,6 +12,7 @@ void setWebView({{package-unescaped}}.RustWebView); java.lang.Class getAppClass(...); java.lang.String getVersion(); + int startActivity(...); } -keep class {{package-unescaped}}.Ipc { diff --git a/src/android/main_pipe.rs b/src/android/main_pipe.rs index 82c526bb8..e1ff06b8b 100644 --- a/src/android/main_pipe.rs +++ b/src/android/main_pipe.rs @@ -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, Receiver), - pub pipe: Arc<[OwnedFd; 2]>, pub activity: GlobalRef, + pub window_manager: GlobalRef, pub webview: Option, 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 { } 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 { - ACTIVITY_PROXY.lock().unwrap().keys().next_back().cloned() +pub fn activity_id_for_window_manager(window_manager: JObject) -> Option { + 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 { @@ -89,28 +122,17 @@ pub fn get_webview(activity_id: ActivityId) -> Option { .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::(); - 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 { - 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(()) } } diff --git a/src/android/mod.rs b/src/android/mod.rs index 4925ec2b4..6f26f703f 100644 --- a/src/android/mod.rs +++ b/src/android/mod.rs @@ -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::(); - 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::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 { - 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, From ae9fe88ed73a98dbbdbe3282912e0bf556fc09dd Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Tue, 28 Oct 2025 18:11:18 -0300 Subject: [PATCH 3/9] fix autoresizing for multiwindow iOS flexible margin somehow makes the window centered at the bottom (pushes all edges) --- src/wkwebview/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wkwebview/mod.rs b/src/wkwebview/mod.rs index 7ca2e14a0..38d6f4f0b 100644 --- a/src/wkwebview/mod.rs +++ b/src/wkwebview/mod.rs @@ -474,9 +474,9 @@ impl InnerWebView { } #[cfg(target_os = "ios")] { - // set all autoresizingmasks - webview.setAutoresizingMask(UIViewAutoresizing::from_bits(31).unwrap()); - // let () = msg_send![webview, setAutoresizingMask: 31]; + webview.setAutoresizingMask( + UIViewAutoresizing::FlexibleWidth | UIViewAutoresizing::FlexibleHeight, + ); // disable scroll bounce by default // https://developer.apple.com/documentation/webkit/wkwebview/1614784-scrollview?language=objc From cde5022514036a8e56756cc269b280b2bf3c50bd Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Mon, 17 Nov 2025 09:01:43 -0300 Subject: [PATCH 4/9] change file --- .changes/multi-window-mobile.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/multi-window-mobile.md diff --git a/.changes/multi-window-mobile.md b/.changes/multi-window-mobile.md new file mode 100644 index 000000000..9d73ea6c7 --- /dev/null +++ b/.changes/multi-window-mobile.md @@ -0,0 +1,5 @@ +--- +"wry": minor +--- + +Update to latest tao to support multi-window on Android and iOS. From 9ded456dfa64b74ff45c78e9b37dde080ca66b64 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Tue, 18 Nov 2025 07:06:50 -0300 Subject: [PATCH 5/9] fix android --- src/android/kotlin/WryActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/android/kotlin/WryActivity.kt b/src/android/kotlin/WryActivity.kt index d3aeef0d7..ef22d846b 100644 --- a/src/android/kotlin/WryActivity.kt +++ b/src/android/kotlin/WryActivity.kt @@ -59,7 +59,7 @@ abstract class WryActivity : AppCompatActivity() { if (handleBackNavigation) { val callback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - if (::this@WryActivity.mWebView.isInitialized) { + if (this@WryActivity::mWebView.isInitialized) { if (this@WryActivity.mWebView.canGoBack()) { this@WryActivity.mWebView.goBack() } else { From b69d100e6bf5729bbc4d1c06073839c204d95ebf Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Tue, 18 Nov 2025 08:26:37 -0300 Subject: [PATCH 6/9] disable cache for custom protocol, fixing load error --- src/android/kotlin/RustWebViewClient.kt | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/android/kotlin/RustWebViewClient.kt b/src/android/kotlin/RustWebViewClient.kt index 43c63adc4..3dae46089 100644 --- a/src/android/kotlin/RustWebViewClient.kt +++ b/src/android/kotlin/RustWebViewClient.kt @@ -15,6 +15,7 @@ import androidx.webkit.WebViewAssetLoader class RustWebViewClient(webView: RustWebView, context: Context): WebViewClient() { private val interceptedState = mutableMapOf() var currentUrl: String = "about:blank" + private var lastInterceptedUrl: Uri? = null private var pendingUrlRedirect: String? = null private val assetLoader = WebViewAssetLoader.Builder() @@ -34,10 +35,18 @@ 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 response = Rust.handleRequest(view.id, request, view.isDocumentStartScriptEnabled) + if (response != null) { + if (response.responseHeaders != null) { + response.responseHeaders["Cache-Control"] = "no-store" + } else { + response.responseHeaders = mapOf("Cache-Control" to "no-store") + } + } interceptedState[request.url.toString()] = response != null return response } @@ -73,17 +82,13 @@ 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 - // - // 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) { + if (error.errorCode == ERROR_CONNECT && request.isForMainFrame && request.url != lastInterceptedUrl) { // prevent the default error page from showing view.stopLoading() // without this initial loadUrl the app is stuck - view.loadUrl(currentUrl) + view.loadUrl(request.url.toString()) // ensure the URL is actually loaded - for some reason there's a race condition and we need to call loadUrl() again later - pendingUrlRedirect = currentUrl + pendingUrlRedirect = request.url.toString() } else { super.onReceivedError(view, request, error) } From 8d8836116aaaef271775e2958f3e38c4b5d18b04 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Tue, 2 Dec 2025 09:05:54 -0300 Subject: [PATCH 7/9] remove activity proxy on destroy --- src/android/main_pipe.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/android/main_pipe.rs b/src/android/main_pipe.rs index e1ff06b8b..cc5a2e85c 100644 --- a/src/android/main_pipe.rs +++ b/src/android/main_pipe.rs @@ -68,6 +68,10 @@ pub fn activity_proxy(id: ActivityId) -> Option { ACTIVITY_PROXY.lock().unwrap().get(&id).cloned() } +fn remove_activity_proxy(id: ActivityId) { + ACTIVITY_PROXY.lock().unwrap().remove(&id); +} + pub fn register_activity_proxy( vm: JavaVM, id: ActivityId, @@ -491,6 +495,7 @@ impl<'a> MainPipe<'a> { // e.g. rotation, multi-window mode change, etc if !is_changing_configurations { super::destroy_webview(activity_id, &webview_id); + remove_activity_proxy(activity_id); } } } From 0904dc98d630157d344178dfb7349e8bb34741ab Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Wed, 3 Dec 2025 14:07:27 -0300 Subject: [PATCH 8/9] call onPause and onResume --- src/android/kotlin/WryActivity.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/android/kotlin/WryActivity.kt b/src/android/kotlin/WryActivity.kt index ef22d846b..9678d8232 100644 --- a/src/android/kotlin/WryActivity.kt +++ b/src/android/kotlin/WryActivity.kt @@ -127,6 +127,20 @@ abstract class WryActivity : AppCompatActivity() { Rust.onActivitySaveInstanceState() } + override fun onPause() { + super.onPause() + if (::mWebView.isInitialized) { + mWebView.onPause() + } + } + + override fun onResume() { + super.onResume() + if (::mWebView.isInitialized) { + mWebView.onResume() + } + } + override fun onDestroy() { super.onDestroy() Rust.onActivityDestroy(this) From 5c83d7bba62a4fc8ef9d76d829b4961a36d0255d Mon Sep 17 00:00:00 2001 From: FabianLars <30730186+FabianLars@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:59:12 +0100 Subject: [PATCH 9/9] use once_cell --- src/android/main_pipe.rs | 6 ++---- src/android/mod.rs | 13 +++++-------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/android/main_pipe.rs b/src/android/main_pipe.rs index cc5a2e85c..52308fd86 100644 --- a/src/android/main_pipe.rs +++ b/src/android/main_pipe.rs @@ -59,10 +59,8 @@ impl ActivityProxy { } } -lazy_static::lazy_static! { - static ref ACTIVITY_PROXY: Mutex> = - Mutex::new(BTreeMap::new()); -} +static ACTIVITY_PROXY: once_cell::sync::Lazy>> = + Lazy::new(|| Mutex::new(BTreeMap::new())); pub fn activity_proxy(id: ActivityId) -> Option { ACTIVITY_PROXY.lock().unwrap().get(&id).cloned() diff --git a/src/android/mod.rs b/src/android/mod.rs index 0c0384c13..a1af12ab8 100644 --- a/src/android/mod.rs +++ b/src/android/mod.rs @@ -4,7 +4,7 @@ use super::{PageLoadEvent, WebViewAttributes, RGBA}; use crate::{ - custom_protocol_workaround, inject_initialization_scripts::inject_scripts_into_html, + custom_protocol_workaround, inject_initialization_scripts::inject_scripts_into_html, Error, RequestAsyncResponder, Result, }; use crossbeam_channel::*; @@ -16,7 +16,7 @@ use jni::{ JNIEnv, }; use ndk::looper::{FdEvent, ThreadLooper}; -use once_cell::sync::OnceCell; +use once_cell::sync::{Lazy, OnceCell}; use raw_window_handle::HasWindowHandle; use std::{ borrow::Cow, @@ -47,15 +47,12 @@ type WebviewId = String; macro_rules! define_static_handlers { ($($key: ident, $var:ident = $type_name:ident);+ $(;)?) => { - $(lazy_static::lazy_static! { - static ref $var: Mutex> = Mutex::new(HashMap::new()); - })* + $(static $var: Lazy>> = Lazy::new(||Mutex::new(HashMap::new()));)* }; ($($var:ident = $type_name:ident { $($fields:ident:$types:ty),+ $(,)? });+ $(;)?) => { - $(lazy_static::lazy_static! { - static ref $var: Mutex> = Mutex::new(HashMap::new()); - } + $( + static $var: Lazy>> = Lazy::new(||Mutex::new(HashMap::new())); pub struct $type_name { $($fields: $types,)* }