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. diff --git a/src/android/binding.rs b/src/android/binding.rs index 8e7156834..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,12 +35,19 @@ 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, wryCreate, []); + android_fn!( + $domain, + $package, + Rust, + onWebviewDestroy, + [JObject, JString] + ); android_fn!( $domain, $package, - RustWebViewClient, + Rust, handleRequest, [JString, JObject, jboolean], jobject @@ -45,57 +55,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 +96,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 +152,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 +180,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 +251,54 @@ fn handle_request( } #[allow(non_snake_case)] -pub unsafe fn onActivityDestroy(_: JNIEnv, _: JClass, _: JObject) { - super::MainPipe::send(super::WebViewMessage::OnDestroy); +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 + .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 +316,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 +361,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 +373,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 +449,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..b3126bb1c --- /dev/null +++ b/src/android/kotlin/Rust.kt @@ -0,0 +1,44 @@ +// 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 wryCreate() + @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..3dae46089 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,17 @@ 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(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 } @@ -50,7 +56,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 +67,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 +94,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 73291e44f..9678d8232 100644 --- a/src/android/kotlin/WryActivity.kt +++ b/src/android/kotlin/WryActivity.kt @@ -4,17 +4,51 @@ package {{package}} -import {{package}}.RustWebView import android.annotation.SuppressLint +import android.content.Intent import android.os.Build import android.os.Bundle import android.webkit.WebView import android.view.KeyEvent import androidx.activity.OnBackPressedCallback 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() + Rust.wryCreate() + } + + 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) { } @@ -25,12 +59,14 @@ abstract class WryActivity : AppCompatActivity() { if (handleBackNavigation) { val callback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - if (this@WryActivity.mWebView.canGoBack()) { - this@WryActivity.mWebView.goBack() - } else { - this.isEnabled = false - this@WryActivity.onBackPressed() - this.isEnabled = true + if (this@WryActivity::mWebView.isInitialized) { + if (this@WryActivity.mWebView.canGoBack()) { + this@WryActivity.mWebView.goBack() + } else { + this.isEnabled = false + this@WryActivity.onBackPressed() + this.isEnabled = true + } } } } @@ -75,70 +111,58 @@ abstract class WryActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - create(this) + id = savedInstanceState?.getInt(ACTIVITY_ID_KEY) ?: intent.extras?.getInt(ACTIVITY_ID_KEY) ?: hashCode() + ProcessLifecycleOwner.get().lifecycle.addObserver(WryLifecycleObserver) + Rust.onActivityCreate(this) } - override fun onStart() { - super.onStart() - start() + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + Rust.onWindowFocusChanged(this, hasFocus) } - override fun onResume() { - super.onResume() - resume() + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putInt(ACTIVITY_ID_KEY, id) + Rust.onActivitySaveInstanceState() } override fun onPause() { super.onPause() - pause() - } - - override fun onStop() { - super.onStop() - stop() - } - - override fun onWindowFocusChanged(hasFocus: Boolean) { - super.onWindowFocusChanged(hasFocus) - focus(hasFocus) + if (::mWebView.isInitialized) { + mWebView.onPause() + } } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - save() + override fun onResume() { + super.onResume() + if (::mWebView.isInitialized) { + mWebView.onResume() + } } 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() } fun getAppClass(name: String): Class<*> { return Class.forName(name) } - companion object { - init { - System.loadLibrary("{{library}}") - } + 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 } - 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/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 5b81048ef..52308fd86 100644 --- a/src/android/main_pipe.rs +++ b/src/android/main_pipe.rs @@ -7,36 +7,131 @@ use crossbeam_channel::*; use jni::{ errors::Result as JniResult, objects::{GlobalRef, JMap, JObject, JString}, - JNIEnv, + JNIEnv, JavaVM, }; use once_cell::sync::Lazy; -use std::os::unix::prelude::*; +use std::{ + collections::BTreeMap, + ffi::c_void, + os::unix::prelude::*, + sync::{Arc, Mutex}, +}; + +use super::{find_class, EvalCallback, WebviewId, EVAL_CALLBACKS, EVAL_ID_GENERATOR, PACKAGE}; -use super::{find_class, EvalCallback, EVAL_CALLBACKS, EVAL_ID_GENERATOR, PACKAGE}; +pub type ActivityId = i32; -static CHANNEL: Lazy<(Sender, Receiver)> = Lazy::new(|| bounded(8)); +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)) } }); -pub enum MainPipeState { - Alive, - Destroyed, +#[derive(Clone)] +pub struct ActivityProxy { + 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( + vm: JavaVM, + activity: GlobalRef, + window_manager: GlobalRef, + webchrome_client: GlobalRef, + ) -> Self { + Self { + activity, + window_manager, + webview: None, + webchrome_client, + java_vm: vm.get_java_vm_pointer() as *mut _, + } + } +} + +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() +} + +fn remove_activity_proxy(id: ActivityId) { + ACTIVITY_PROXY.lock().unwrap().remove(&id); +} + +pub fn register_activity_proxy( + vm: JavaVM, + id: ActivityId, + activity: GlobalRef, + window_manager: GlobalRef, + webchrome_client: GlobalRef, +) { + 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.java_vm = vm.get_java_vm_pointer() as *mut _; + } else { + let proxy = ActivityProxy::new(vm, activity, window_manager, webchrome_client); + activity_proxy.insert(id, proxy.clone()); + } +} + +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 { + 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 struct MainPipe<'a> { pub env: JNIEnv<'a>, - pub activity: GlobalRef, - pub webview: Option, - pub webchrome_client: GlobalRef, } 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) { + if CHANNEL.0.send((activity_id, message)).is_ok() { unsafe { libc::write( MAIN_PIPE[1].as_raw_fd(), @@ -47,11 +142,17 @@ impl<'a> MainPipe<'a> { } } - pub fn recv(&mut self) -> JniResult { - let activity = self.activity.as_obj(); - if let Ok(message) = CHANNEL.1.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(activity_id).map(|p| (p.activity.clone(), p.webchrome_client.clone())) + else { + #[cfg(debug_assertions)] + eprintln!("no activity found for activity id: {}", activity_id); + return Ok(()); + }; let CreateWebViewAttributes { url, html, @@ -82,25 +183,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 +209,6 @@ impl<'a> MainPipe<'a> { &[], )? .l()?; - // set media autoplay self.env.call_method( &web_settings, @@ -119,7 +216,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 +237,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 +254,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 +262,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 +282,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 +311,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(&activity_id) + .unwrap() + .webview + .replace(webview); } WebViewMessage::Eval(script, callback) => { - if let Some(webview) = &self.webview { + if let Some(webview) = get_webview(activity_id) { let id = EVAL_ID_GENERATOR.next() as i32; #[cfg(feature = "tracing")] @@ -275,30 +373,34 @@ impl<'a> MainPipe<'a> { } } WebViewMessage::SetBackgroundColor(background_color) => { - if let Some(webview) = &self.webview { + if let Some(webview) = get_webview(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(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(activity_id) { let url = self .env .call_method(webview.as_obj(), "getUrl", "()Ljava/lang/String;", &[]) @@ -316,38 +418,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(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(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(activity_id) { self .env .call_method(webview, "clearAllBrowsingData", "()V", &[])?; } } WebViewMessage::LoadHtml(html) => { - if let Some(webview) = &self.webview { + 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) = &self.webview { + if let Some(webview) = get_webview(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(activity_id) { let url = self.env.new_string(url)?; let cookies = self .env @@ -376,12 +484,21 @@ 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); + remove_activity_proxy(activity_id); + } } } } - Ok(MainPipeState::Alive) + Ok(()) } } @@ -457,9 +574,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 +592,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 cdb086c4e..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,19 +16,21 @@ 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, collections::HashMap, - os::fd::{AsFd as _, AsRawFd as _}, sync::{mpsc::channel, Mutex}, time::Duration, }; pub(crate) mod binding; mod main_pipe; -use main_pipe::{CreateWebViewAttributes, MainPipe, MainPipeState, WebViewMessage, MAIN_PIPE}; +use main_pipe::{ + activity_id_for_window_manager, first_activity_id, register_activity_proxy, ActivityId, + CreateWebViewAttributes, MainPipe, WebViewMessage, +}; use crate::util::Counter; @@ -41,22 +43,16 @@ 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);+ $(;)?) => { + $(static $var: Lazy>> = Lazy::new(||Mutex::new(HashMap::new()));)* + }; + ($($var:ident = $type_name:ident { $($fields:ident:$types:ty),+ $(,)? });+ $(;)?) => { - $(pub static $var: StaticValue> = StaticValue(Mutex::new(None)); + $( + static $var: Lazy>> = Lazy::new(||Mutex::new(HashMap::new())); pub struct $type_name { $($fields: $types,)* } @@ -73,15 +69,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(); @@ -90,6 +88,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, @@ -97,11 +106,31 @@ pub static EVAL_CALLBACKS: OnceCell>> = OnceCel 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( @@ -113,53 +142,51 @@ 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(); + 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()), + ); + } } pub(crate) struct InnerWebView { id: String, + pub activity_id: ActivityId, } 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 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, @@ -203,79 +230,90 @@ 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, custom_protocol_handler)) = - custom_protocols.iter().find(|(protocol, _)| { - custom_protocol_workaround::is_work_around_uri(&uri, http_or_https, protocol) - }) - { - let uri_res = custom_protocol_workaround::revert_uri_work_around( - &uri, - http_or_https, - custom_protocol, - ) - .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"); - response = inject_scripts_into_html(response, &initialization_scripts); + .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, custom_protocol_handler)) = + custom_protocols.iter().find(|(protocol, _)| { + custom_protocol_workaround::is_work_around_uri(&uri, http_or_https, protocol) + }) + { + let uri_res = custom_protocol_workaround::revert_uri_work_around( + &uri, + http_or_https, + custom_protocol, + ) + .parse(); + + if let Ok(uri) = uri_res { + *request.uri_mut() = uri; } - let _ = tx.send(response); - }); - - (custom_protocol_handler)(webview_id, request, RequestAsyncResponder { responder }); - return Some(rx.recv_timeout(MAIN_PIPE_TIMEOUT).unwrap()); - } - None - }, + + 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"); + response = inject_scripts_into_html(response, &initialization_scripts); + } + let _ = tx.send(response); + }); + + (custom_protocol_handler)(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, @@ -289,9 +327,16 @@ impl InnerWebView { user_agent, initialization_scripts, javascript_disabled, - })); + }; + + WEBVIEW_ATTRIBUTES + .lock() + .unwrap() + .insert(activity_id, attributes.clone()); - Ok(Self { id }) + MainPipe::send(activity_id, WebViewMessage::CreateWebView(attributes)); + + Ok(Self { id, activity_id }) } pub fn print(&self) -> crate::Result<()> { @@ -304,15 +349,18 @@ impl InnerWebView { 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(()) } @@ -332,38 +380,50 @@ 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) } @@ -407,7 +467,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. @@ -416,13 +478,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() } @@ -451,5 +521,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 de979345c..d10437932 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1848,8 +1848,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, @@ -1858,7 +1864,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, @@ -1885,12 +1894,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 } @@ -2498,7 +2510,9 @@ pub trait WebViewExtAndroid { #[cfg(target_os = "android")] impl WebViewExtAndroid for WebView { fn handle(&self) -> JniHandle { - JniHandle + JniHandle { + activity_id: self.webview.activity_id, + } } } diff --git a/src/wkwebview/mod.rs b/src/wkwebview/mod.rs index 3cd656042..685634980 100644 --- a/src/wkwebview/mod.rs +++ b/src/wkwebview/mod.rs @@ -518,9 +518,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