fix(tauri): patch wry 绕过 macOS 26 启动崩溃(NSBundle bundleWithIdentifier SIGTRAP)

- 根因:tauri 启动探测 webview 版本时调用 wry platform_webview_version()
  → NSBundle::bundleWithIdentifier(com.apple.WebKit) 在 macOS 26 (Tahoe) 上
  触发 CFBundle/CFRelease 断言崩溃(EXC_BREAKPOINT)
- 方案:vendor 本地 wry 0.55.1,该函数改为安全占位返回(仅被上层用作 is_ok() 探测,
  返回值无实际用途),[patch.crates-io] 指向本地副本
- 附带:移除 NSBundle import,消除 unused 警告
This commit is contained in:
Pine
2026-08-18 01:53:34 +08:00
parent d26540193e
commit a143b94ac1
63 changed files with 21288 additions and 4 deletions
+483
View File
@@ -0,0 +1,483 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use http::{
header::{HeaderName, HeaderValue, CONTENT_LENGTH, CONTENT_TYPE},
Request,
};
use jni::errors::Result as JniResult;
pub use jni::{
self,
objects::{GlobalRef, JClass, JMap, JObject, JString},
sys::{jboolean, jint, jobject, jstring},
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,
};
use crate::PageLoadEvent;
#[macro_export]
macro_rules! android_binding {
($domain:ident, $package:ident) => {
::wry::android_binding!($domain, $package, ::wry)
};
// use imported `android_setup` just to force the import path to use `wry::{}`
// as the macro breaks without braces
($domain:ident, $package:ident, $wry:path) => {{
use $wry::{android_setup as _, prelude::*};
android_fn!($domain, $package, Rust, wryCreate, []);
android_fn!(
$domain,
$package,
Rust,
onWebviewDestroy,
[JObject, JString]
);
android_fn!(
$domain,
$package,
Rust,
handleRequest,
[JString, JObject, jboolean],
jobject
);
android_fn!(
$domain,
$package,
Rust,
withAssetLoader,
[JString],
jboolean
);
android_fn!(
$domain,
$package,
Rust,
assetLoaderDomain,
[JString],
jstring
);
android_fn!(
$domain,
$package,
Rust,
shouldOverride,
[JString, JString],
jboolean
);
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,
Rust,
handleReceivedTitle,
[JString, JString],
);
}};
}
fn handle_request(
env: &mut JNIEnv,
webview_id: JString,
request: JObject,
is_document_start_script_enabled: jboolean,
) -> JniResult<jobject> {
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();
let mut request_builder = Request::builder();
let uri = env
.call_method(&request, "getUrl", "()Landroid/net/Uri;", &[])?
.l()?;
let url: JString = env
.call_method(&uri, "toString", "()Ljava/lang/String;", &[])?
.l()?
.into();
let url = env.get_string(&url)?.to_string_lossy().to_string();
#[cfg(feature = "tracing")]
span.record("uri", &url);
request_builder = request_builder.uri(&url);
let method = env
.call_method(&request, "getMethod", "()Ljava/lang/String;", &[])?
.l()
.map(JString::from)?;
request_builder = request_builder.method(
env
.get_string(&method)?
.to_string_lossy()
.to_string()
.as_str(),
);
let request_headers = env
.call_method(request, "getRequestHeaders", "()Ljava/util/Map;", &[])?
.l()?;
let request_headers = JMap::from_env(env, &request_headers)?;
let mut iter = request_headers.iter(env)?;
while let Some((header, value)) = iter.next(env)? {
let header = JString::from(header);
let value = JString::from(value);
let header = env.get_string(&header)?;
let value = env.get_string(&value)?;
if let (Ok(header), Ok(value)) = (
HeaderName::from_bytes(header.to_bytes()),
HeaderValue::from_bytes(value.to_bytes()),
) {
request_builder = request_builder.header(header, value);
}
}
let final_request = match request_builder.body(Vec::new()) {
Ok(req) => req,
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to build response: {_e}");
return Ok(*JObject::null());
}
};
let response = {
#[cfg(feature = "tracing")]
let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered();
(handler.handler)(
webview_id,
final_request,
is_document_start_script_enabled != 0,
)
};
if let Some(response) = response {
let status = response.status();
let status_code = status.as_u16() as i32;
let status_err = if status_code < 100 {
Some("Status code can't be less than 100")
} else if status_code > 599 {
Some("statusCode can't be greater than 599.")
} else if status_code > 299 && status_code < 400 {
Some("statusCode can't be in the [300, 399] range.")
} else {
None
};
if let Some(_err) = status_err {
#[cfg(feature = "tracing")]
tracing::warn!("{_err}");
return Ok(*JObject::null());
}
let reason_phrase = status.canonical_reason().unwrap_or("OK");
let (mime_type, encoding) = if let Some(content_type) = response.headers().get(CONTENT_TYPE) {
let content_type = content_type.to_str().unwrap().trim();
let mut s = content_type.split(';');
let mime_type = s.next().unwrap().trim();
let mut encoding = None;
for token in s {
let token = token.trim();
if token.starts_with("charset=") {
encoding.replace(token.split('=').nth(1).unwrap());
break;
}
}
(
env.new_string(mime_type)?,
if let Some(encoding) = encoding {
env.new_string(encoding)?
} else {
JString::default()
},
)
} else {
(JString::default(), JString::default())
};
let headers = response.headers();
let obj = env.new_object("java/util/HashMap", "()V", &[])?;
let response_headers = {
let headers_map = JMap::from_env(env, &obj)?;
for (name, value) in headers.iter() {
// WebResourceResponse will automatically generate Content-Type and
// Content-Length headers so we should skip them to avoid duplication.
if name == CONTENT_TYPE || name == CONTENT_LENGTH {
continue;
}
let key = env.new_string(name)?;
let value = env.new_string(value.to_str().unwrap_or_default())?;
headers_map.put(env, &key, &value)?;
}
headers_map
};
let bytes = response.body();
let byte_array_input_stream = env.find_class("java/io/ByteArrayInputStream")?;
let byte_array = env.byte_array_from_slice(bytes)?;
let stream = env.new_object(byte_array_input_stream, "([B)V", &[(&byte_array).into()])?;
let reason_phrase = env.new_string(reason_phrase)?;
let web_resource_response_class = env.find_class("android/webkit/WebResourceResponse")?;
let web_resource_response = env.new_object(
web_resource_response_class,
"(Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/util/Map;Ljava/io/InputStream;)V",
&[(&mime_type).into(), (&encoding).into(), status_code.into(), (&reason_phrase).into(), (&response_headers).into(), (&stream).into()],
)?;
return Ok(*web_resource_response);
}
}
Ok(*JObject::null())
}
#[allow(non_snake_case)]
pub unsafe fn wryCreate(env: JNIEnv, _: JClass) {
let mut main_pipe = MainPipe { env };
let looper = ThreadLooper::for_thread().unwrap();
looper
.add_fd_with_callback(MAIN_PIPE[0].as_fd(), FdEvent::INPUT, move |fd, _event| {
let size = std::mem::size_of::<bool>();
let mut wake = false;
if libc::read(fd.as_raw_fd(), &mut wake as *mut _ as *mut _, size) == size as libc::ssize_t {
// unregister itself on errors
main_pipe.recv().is_ok()
} else {
// unregister itself
false
}
})
.unwrap();
}
#[allow(non_snake_case)]
pub unsafe fn onWebviewDestroy(mut env: JNIEnv, _: JClass, activity: JObject, webview_id: JString) {
let activity_id = env
.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)]
pub unsafe fn handleRequest(
mut env: JNIEnv,
_: JClass,
webview_id: JString,
request: JObject,
is_document_start_script_enabled: jboolean,
) -> jobject {
match handle_request(
&mut env,
webview_id,
request,
is_document_start_script_enabled,
) {
Ok(response) => response,
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to handle request: {_e}");
JObject::null().as_raw()
}
}
}
#[allow(non_snake_case)]
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()
.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_default()
}
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}");
false
}
}
.into()
}
#[allow(non_snake_case)]
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
.get_or_init(Default::default)
.lock()
.unwrap()
.get(&id)
{
cb(result.into());
}
}
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}");
}
}
}
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();
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)) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}")
}
}
}
#[allow(non_snake_case)]
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();
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)) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}")
}
}
}
#[allow(non_snake_case)]
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(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()
}
}
#[allow(non_snake_case)]
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();
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)) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}")
}
}
}
#[allow(non_snake_case)]
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();
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)) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}")
}
}
}
+23
View File
@@ -0,0 +1,23 @@
// 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.*
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
Rust.ipc(webView.id, webViewClient.currentUrl, m)
}
}
{{class-extension}}
}
+87
View File
@@ -0,0 +1,87 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package {{package}}
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/Logger.java
import android.text.TextUtils
import android.util.Log
class Logger {
companion object {
private const val LOG_TAG_CORE = "Tauri"
fun tags(vararg subtags: String): String {
return if (subtags.isNotEmpty()) {
LOG_TAG_CORE + "/" + TextUtils.join("/", subtags)
} else LOG_TAG_CORE
}
fun verbose(message: String) {
verbose(LOG_TAG_CORE, message)
}
private fun verbose(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.v(tag, message)
}
fun debug(message: String) {
debug(LOG_TAG_CORE, message)
}
fun debug(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.d(tag, message)
}
fun info(message: String) {
info(LOG_TAG_CORE, message)
}
fun info(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.i(tag, message)
}
fun warn(message: String) {
warn(LOG_TAG_CORE, message)
}
fun warn(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.w(tag, message)
}
fun error(message: String) {
error(LOG_TAG_CORE, message, null)
}
fun error(message: String, e: Throwable?) {
error(LOG_TAG_CORE, message, e)
}
fun error(tag: String, message: String, e: Throwable?) {
if (!shouldLog()) {
return
}
Log.e(tag, message, e)
}
private fun shouldLog(): Boolean {
return BuildConfig.DEBUG
}
}
}
@@ -0,0 +1,115 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package {{package}}
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/PermissionHelper.java
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.ActivityCompat
import java.util.ArrayList
object PermissionHelper {
/**
* Checks if a list of given permissions are all granted by the user
*
* @param permissions Permissions to check.
* @return True if all permissions are granted, false if at least one is not.
*/
fun hasPermissions(context: Context?, permissions: Array<String>): Boolean {
for (perm in permissions) {
if (ActivityCompat.checkSelfPermission(
context!!,
perm
) != PackageManager.PERMISSION_GRANTED
) {
return false
}
}
return true
}
/**
* Check whether the given permission has been defined in the AndroidManifest.xml
*
* @param permission A permission to check.
* @return True if the permission has been defined in the Manifest, false if not.
*/
fun hasDefinedPermission(context: Context, permission: String): Boolean {
var hasPermission = false
val requestedPermissions = getManifestPermissions(context)
if (!requestedPermissions.isNullOrEmpty()) {
val requestedPermissionsList = listOf(*requestedPermissions)
val requestedPermissionsArrayList = ArrayList(requestedPermissionsList)
if (requestedPermissionsArrayList.contains(permission)) {
hasPermission = true
}
}
return hasPermission
}
/**
* Check whether all of the given permissions have been defined in the AndroidManifest.xml
* @param context the app context
* @param permissions a list of permissions
* @return true only if all permissions are defined in the AndroidManifest.xml
*/
fun hasDefinedPermissions(context: Context, permissions: Array<String>): Boolean {
for (permission in permissions) {
if (!hasDefinedPermission(context, permission)) {
return false
}
}
return true
}
/**
* Get the permissions defined in AndroidManifest.xml
*
* @return The permissions defined in AndroidManifest.xml
*/
private fun getManifestPermissions(context: Context): Array<String>? {
var requestedPermissions: Array<String>? = null
try {
val pm = context.packageManager
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
} else {
@Suppress("DEPRECATION")
pm.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)
}
if (packageInfo != null) {
requestedPermissions = packageInfo.requestedPermissions
}
} catch (_: Exception) {
}
return requestedPermissions
}
/**
* Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml
*
* @param neededPermissions The permissions needed.
* @return The permissions not present in AndroidManifest.xml
*/
fun getUndefinedPermissions(context: Context, neededPermissions: Array<String?>): Array<String?> {
val undefinedPermissions = ArrayList<String?>()
val requestedPermissions = getManifestPermissions(context)
if (!requestedPermissions.isNullOrEmpty()) {
val requestedPermissionsList = listOf(*requestedPermissions)
val requestedPermissionsArrayList = ArrayList(requestedPermissionsList)
for (permission in neededPermissions) {
if (!requestedPermissionsArrayList.contains(permission)) {
undefinedPermissions.add(permission)
}
}
var undefinedPermissionArray = arrayOfNulls<String>(undefinedPermissions.size)
undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray)
return undefinedPermissionArray
}
return neededPermissions
}
}
+46
View File
@@ -0,0 +1,46 @@
// 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.content.Intent
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 onNewIntent(intent: Intent)
@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)
}
@@ -0,0 +1,491 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
@file:Suppress("ObsoleteSdkInt", "RedundantOverride", "QueryPermissionsNeeded", "SimpleDateFormat")
package {{package}}
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java
import android.Manifest
import android.app.Activity
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.view.View
import android.webkit.*
import android.widget.EditText
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() {
private interface PermissionListener {
fun onPermissionSelect(isGranted: Boolean?)
}
private interface ActivityResultListener {
fun onActivityResult(result: ActivityResult?)
}
private val activity: WryActivity
private var permissionLauncher: ActivityResultLauncher<Array<String>>
private var activityLauncher: ActivityResultLauncher<Intent>
private var permissionListener: PermissionListener? = null
private var activityListener: ActivityResultListener? = null
init {
activity = appActivity
val permissionCallback =
ActivityResultCallback { isGranted: Map<String, Boolean> ->
if (permissionListener != null) {
var granted = true
for ((_, value) in isGranted) {
if (!value) granted = false
}
permissionListener!!.onPermissionSelect(granted)
}
}
permissionLauncher =
activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions(), permissionCallback)
activityLauncher = activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (activityListener != null) {
activityListener!!.onActivityResult(result)
}
}
}
/**
* Render web content in `view`.
*
* Both this method and [.onHideCustomView] are required for
* rendering web content in full screen.
*
* @see [](https://developer.android.com/reference/android/webkit/WebChromeClient.onShowCustomView
) */
override fun onShowCustomView(view: View, callback: CustomViewCallback) {
callback.onCustomViewHidden()
super.onShowCustomView(view, callback)
}
/**
* Render web content in the original Web View again.
*
* Do not remove this method--@see #onShowCustomView(View, CustomViewCallback).
*/
override fun onHideCustomView() {
super.onHideCustomView()
}
override fun onPermissionRequest(request: PermissionRequest) {
val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
val permissionList: MutableList<String> = ArrayList()
if (listOf(*request.resources).contains("android.webkit.resource.VIDEO_CAPTURE")) {
permissionList.add(Manifest.permission.CAMERA)
}
if (listOf(*request.resources).contains("android.webkit.resource.AUDIO_CAPTURE")) {
permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS)
permissionList.add(Manifest.permission.RECORD_AUDIO)
}
if (permissionList.isNotEmpty() && isRequestPermissionRequired) {
val permissions = permissionList.toTypedArray()
permissionListener = object : PermissionListener {
override fun onPermissionSelect(isGranted: Boolean?) {
if (isGranted == true) {
request.grant(request.resources)
} else {
request.deny()
}
}
}
permissionLauncher.launch(permissions)
} else {
request.grant(request.resources)
}
}
/**
* Show the browser alert modal
* @param view
* @param url
* @param message
* @param result
* @return
*/
override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean {
if (activity.isFinishing) {
return true
}
val builder = AlertDialog.Builder(view.context)
builder
.setMessage(message)
.setPositiveButton(
"OK"
) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
result.confirm()
}
.setOnCancelListener { dialog: DialogInterface ->
dialog.dismiss()
result.cancel()
}
val dialog = builder.create()
dialog.show()
return true
}
/**
* Show the browser confirm modal
* @param view
* @param url
* @param message
* @param result
* @return
*/
override fun onJsConfirm(view: WebView, url: String, message: String, result: JsResult): Boolean {
if (activity.isFinishing) {
return true
}
val builder = AlertDialog.Builder(view.context)
builder
.setMessage(message)
.setPositiveButton(
"OK"
) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
result.confirm()
}
.setNegativeButton(
"Cancel"
) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
result.cancel()
}
.setOnCancelListener { dialog: DialogInterface ->
dialog.dismiss()
result.cancel()
}
val dialog = builder.create()
dialog.show()
return true
}
/**
* Show the browser prompt modal
* @param view
* @param url
* @param message
* @param defaultValue
* @param result
* @return
*/
override fun onJsPrompt(
view: WebView,
url: String,
message: String,
defaultValue: String,
result: JsPromptResult
): Boolean {
if (activity.isFinishing) {
return true
}
val builder = AlertDialog.Builder(view.context)
val input = EditText(view.context)
builder
.setMessage(message)
.setView(input)
.setPositiveButton(
"OK"
) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
val inputText1 = input.text.toString().trim { it <= ' ' }
result.confirm(inputText1)
}
.setNegativeButton(
"Cancel"
) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
result.cancel()
}
.setOnCancelListener { dialog: DialogInterface ->
dialog.dismiss()
result.cancel()
}
val dialog = builder.create()
dialog.show()
return true
}
/**
* Handle the browser geolocation permission prompt
* @param origin
* @param callback
*/
override fun onGeolocationPermissionsShowPrompt(
origin: String,
callback: GeolocationPermissions.Callback
) {
super.onGeolocationPermissionsShowPrompt(origin, callback)
Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: $origin")
val geoPermissions =
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
if (!PermissionHelper.hasPermissions(activity, geoPermissions)) {
permissionListener = object : PermissionListener {
override fun onPermissionSelect(isGranted: Boolean?) {
if (isGranted == true) {
callback.invoke(origin, true, false)
} else {
val coarsePermission =
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
PermissionHelper.hasPermissions(activity, coarsePermission)
) {
callback.invoke(origin, true, false)
} else {
callback.invoke(origin, false, false)
}
}
}
}
permissionLauncher.launch(geoPermissions)
} else {
// permission is already granted
callback.invoke(origin, true, false)
Logger.debug("onGeolocationPermissionsShowPrompt: has required permission")
}
}
override fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri?>?>,
fileChooserParams: FileChooserParams
): Boolean {
val acceptTypes = listOf(*fileChooserParams.acceptTypes)
val captureEnabled = fileChooserParams.isCaptureEnabled
val capturePhoto = captureEnabled && acceptTypes.contains("image/*")
val captureVideo = captureEnabled && acceptTypes.contains("video/*")
if (capturePhoto || captureVideo) {
if (isMediaCaptureSupported) {
showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo)
} else {
permissionListener = object : PermissionListener {
override fun onPermissionSelect(isGranted: Boolean?) {
if (isGranted == true) {
showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo)
} else {
Logger.warn(Logger.tags("FileChooser"), "Camera permission not granted")
filePathCallback.onReceiveValue(null)
}
}
}
val camPermission = arrayOf(Manifest.permission.CAMERA)
permissionLauncher.launch(camPermission)
}
} else {
showFilePicker(filePathCallback, fileChooserParams)
}
return true
}
private val isMediaCaptureSupported: Boolean
get() {
val permissions = arrayOf(Manifest.permission.CAMERA)
return PermissionHelper.hasPermissions(activity, permissions) ||
!PermissionHelper.hasDefinedPermission(activity, Manifest.permission.CAMERA)
}
private fun showMediaCaptureOrFilePicker(
filePathCallback: ValueCallback<Array<Uri?>?>,
fileChooserParams: FileChooserParams,
isVideo: Boolean
) {
val isVideoCaptureSupported = true
val shown = if (isVideo && isVideoCaptureSupported) {
showVideoCapturePicker(filePathCallback)
} else {
showImageCapturePicker(filePathCallback)
}
if (!shown) {
Logger.warn(
Logger.tags("FileChooser"),
"Media capture intent could not be launched. Falling back to default file picker."
)
showFilePicker(filePathCallback, fileChooserParams)
}
}
private fun showImageCapturePicker(filePathCallback: ValueCallback<Array<Uri?>?>): Boolean {
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
if (takePictureIntent.resolveActivity(activity.packageManager) == null) {
return false
}
val imageFileUri: Uri = try {
createImageFileUri()
} catch (ex: Exception) {
Logger.error("Unable to create temporary media capture file: " + ex.message)
return false
}
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri)
activityListener = object : ActivityResultListener {
override fun onActivityResult(result: ActivityResult?) {
var res: Array<Uri?>? = null
if (result?.resultCode == Activity.RESULT_OK) {
res = arrayOf(imageFileUri)
}
filePathCallback.onReceiveValue(res)
}
}
activityLauncher.launch(takePictureIntent)
return true
}
private fun showVideoCapturePicker(filePathCallback: ValueCallback<Array<Uri?>?>): Boolean {
val takeVideoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
if (takeVideoIntent.resolveActivity(activity.packageManager) == null) {
return false
}
activityListener = object : ActivityResultListener {
override fun onActivityResult(result: ActivityResult?) {
var res: Array<Uri?>? = null
if (result?.resultCode == Activity.RESULT_OK) {
res = arrayOf(result.data!!.data)
}
filePathCallback.onReceiveValue(res)
}
}
activityLauncher.launch(takeVideoIntent)
return true
}
private fun showFilePicker(
filePathCallback: ValueCallback<Array<Uri?>?>,
fileChooserParams: FileChooserParams
) {
val intent = fileChooserParams.createIntent()
if (fileChooserParams.mode == FileChooserParams.MODE_OPEN_MULTIPLE) {
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}
if (fileChooserParams.acceptTypes.size > 1 || intent.type!!.startsWith(".")) {
val validTypes = getValidTypes(fileChooserParams.acceptTypes)
intent.putExtra(Intent.EXTRA_MIME_TYPES, validTypes)
if (intent.type!!.startsWith(".")) {
intent.type = validTypes[0]
}
}
try {
activityListener = object : ActivityResultListener {
override fun onActivityResult(result: ActivityResult?) {
val res: Array<Uri?>?
val resultIntent = result?.data
if (result?.resultCode == Activity.RESULT_OK && resultIntent!!.clipData != null) {
val numFiles = resultIntent.clipData!!.itemCount
res = arrayOfNulls(numFiles)
for (i in 0 until numFiles) {
res[i] = resultIntent.clipData!!.getItemAt(i).uri
}
} else {
res = FileChooserParams.parseResult(
result?.resultCode ?: 0,
resultIntent
)
}
filePathCallback.onReceiveValue(res)
}
}
activityLauncher.launch(intent)
} catch (e: ActivityNotFoundException) {
filePathCallback.onReceiveValue(null)
}
}
private fun getValidTypes(currentTypes: Array<String>): Array<String> {
val validTypes: MutableList<String> = ArrayList()
val mtm = MimeTypeMap.getSingleton()
for (mime in currentTypes) {
if (mime.startsWith(".")) {
val extension = mime.substring(1)
val extensionMime = mtm.getMimeTypeFromExtension(extension)
if (extensionMime != null && !validTypes.contains(extensionMime)) {
validTypes.add(extensionMime)
}
} else if (!validTypes.contains(mime)) {
validTypes.add(mime)
}
}
val validObj: Array<Any> = validTypes.toTypedArray()
return Arrays.copyOf(
validObj, validObj.size,
Array<String>::class.java
)
}
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
val tag: String = Logger.tags("Console")
if (consoleMessage.message() != null && isValidMsg(consoleMessage.message())) {
val msg = String.format(
"File: %s - Line %d - Msg: %s",
consoleMessage.sourceId(),
consoleMessage.lineNumber(),
consoleMessage.message()
)
val level = consoleMessage.messageLevel().name
if ("ERROR".equals(level, ignoreCase = true)) {
Logger.error(tag, msg, null)
} else if ("WARNING".equals(level, ignoreCase = true)) {
Logger.warn(tag, msg)
} else if ("TIP".equals(level, ignoreCase = true)) {
Logger.debug(tag, msg)
} else {
Logger.info(tag, msg)
}
}
return true
}
private fun isValidMsg(msg: String): Boolean {
return !(msg.contains("%cresult %c") ||
msg.contains("%cnative %c") ||
msg.equals("[object Object]", ignoreCase = true) ||
msg.equals("console.groupEnd", ignoreCase = true))
}
@Throws(IOException::class)
private fun createImageFileUri(): Uri {
val photoFile = createImageFile(activity)
return FileProvider.getUriForFile(
activity,
activity.packageName.toString() + ".fileprovider",
photoFile
)
}
@Throws(IOException::class)
private fun createImageFile(activity: Activity): File {
// Create an image file name
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
val imageFileName = "JPEG_" + timeStamp + "_"
val storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile(imageFileName, ".jpg", storageDir)
}
override fun onReceivedTitle(
view: WebView,
title: String
) {
Rust.handleReceivedTitle((view as RustWebView).id, title)
}
}
+96
View File
@@ -0,0 +1,96 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
@file:Suppress("unused", "SetJavaScriptEnabled")
package {{package}}
import android.annotation.SuppressLint
import android.webkit.*
import android.content.Context
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
import kotlin.collections.Map
@SuppressLint("RestrictedApi")
class RustWebView(context: Context, val initScripts: Array<String>, val id: String): WebView(context) {
val isDocumentStartScriptEnabled: Boolean
init {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.setGeolocationEnabled(true)
settings.databaseEnabled = true
settings.mediaPlaybackRequiresUserGesture = false
settings.javaScriptCanOpenWindowsAutomatically = true
if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
isDocumentStartScriptEnabled = true
for (script in initScripts) {
WebViewCompat.addDocumentStartJavaScript(this, script, setOf("*"));
}
} else {
isDocumentStartScriptEnabled = false
}
{{class-init}}
}
fun loadUrlMainThread(url: String) {
post {
loadUrl(url)
}
}
fun loadUrlMainThread(url: String, additionalHttpHeaders: Map<String, String>) {
post {
loadUrl(url, additionalHttpHeaders)
}
}
override fun loadUrl(url: String) {
if (!Rust.shouldOverride(id, url)) {
super.loadUrl(url);
}
}
override fun loadUrl(url: String, additionalHttpHeaders: Map<String, String>) {
if (!Rust.shouldOverride(id, url)) {
super.loadUrl(url, additionalHttpHeaders);
}
}
fun loadHTMLMainThread(html: String) {
post {
super.loadData(html, "text/html", null)
}
}
fun evalScript(id: Int, script: String) {
post {
super.evaluateJavascript(script) { result ->
Rust.onEval(this.id, id, result)
}
}
}
fun clearAllBrowsingData() {
try {
super.getContext().deleteDatabase("webviewCache.db")
super.getContext().deleteDatabase("webview.db")
super.clearCache(true)
super.clearHistory()
super.clearFormData()
} catch (ex: Exception) {
Logger.error("Unable to create temporary media capture file: " + ex.message)
}
}
fun getCookies(url: String): String {
val cookieManager = CookieManager.getInstance()
return cookieManager.getCookie(url)
}
{{class-extension}}
}
@@ -0,0 +1,98 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package {{package}}
import android.net.Uri
import android.webkit.*
import android.content.Context
import android.graphics.Bitmap
import android.os.Handler
import android.os.Looper
import androidx.webkit.WebViewAssetLoader
class RustWebViewClient(webView: RustWebView, context: Context): WebViewClient() {
private val interceptedState = mutableMapOf<String, Boolean>()
var currentUrl: String = "about:blank"
private var lastInterceptedUrl: Uri? = null
private var pendingUrlRedirect: String? = null
private val assetLoader = WebViewAssetLoader.Builder()
.setDomain(Rust.assetLoaderDomain(webView.id))
.addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(context))
.build()
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
pendingUrlRedirect?.let {
Handler(Looper.getMainLooper()).post {
view.loadUrl(it)
}
pendingUrlRedirect = null
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
}
}
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
return Rust.shouldOverride((view as RustWebView).id, request.url.toString())
}
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
currentUrl = url
if (interceptedState[url] == false) {
val webView = view as RustWebView
for (script in webView.initScripts) {
view.evaluateJavascript(script, null)
}
}
return Rust.onPageLoading((view as RustWebView).id, url)
}
override fun onPageFinished(view: WebView, url: String) {
Rust.onPageLoaded((view as RustWebView).id, url)
}
override fun onReceivedError(
view: WebView,
request: WebResourceRequest,
error: WebResourceError
) {
// 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) {
// prevent the default error page from showing
view.stopLoading()
// without this initial loadUrl the app is stuck
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 = request.url.toString()
} else {
super.onReceivedError(view, request, error)
}
}
{{class-extension}}
}
+173
View File
@@ -0,0 +1,173 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package {{package}}
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) { }
fun setWebView(webView: RustWebView) {
mWebView = webView
if (handleBackNavigation) {
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (this@WryActivity::mWebView.isInitialized) {
if (this@WryActivity.mWebView.canGoBack()) {
this@WryActivity.mWebView.goBack()
} else {
this.isEnabled = false
this@WryActivity.onBackPressed()
this.isEnabled = true
}
}
}
}
onBackPressedDispatcher.addCallback(this, callback)
}
onWebViewCreate(webView)
}
val version: String
@SuppressLint("WebViewApiAvailability", "ObsoleteSdkInt")
get() {
// Check getCurrentWebViewPackage() directly if above Android 8
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return WebView.getCurrentWebViewPackage()?.versionName ?: ""
}
// Otherwise manually check WebView versions
var webViewPackage = "com.google.android.webview"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
webViewPackage = "com.android.chrome"
}
try {
@Suppress("DEPRECATION")
val info = packageManager.getPackageInfo(webViewPackage, 0)
return info.versionName.toString()
} catch (ex: Exception) {
Logger.warn("Unable to get package info for '$webViewPackage'$ex")
}
try {
@Suppress("DEPRECATION")
val info = packageManager.getPackageInfo("com.android.webview", 0)
return info.versionName.toString()
} catch (ex: Exception) {
Logger.warn("Unable to get package info for 'com.android.webview'$ex")
}
// Could not detect any webview, return empty string
return ""
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
id = savedInstanceState?.getInt(ACTIVITY_ID_KEY) ?: intent.extras?.getInt(ACTIVITY_ID_KEY) ?: hashCode()
ProcessLifecycleOwner.get().lifecycle.addObserver(WryLifecycleObserver)
Rust.onActivityCreate(this)
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
Rust.onWindowFocusChanged(this, hasFocus)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(ACTIVITY_ID_KEY, id)
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)
Rust.onWebviewDestroy(this, if (::mWebView.isInitialized) { mWebView.id } else { "" })
}
override fun onLowMemory() {
super.onLowMemory()
Rust.onActivityLowMemory()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Rust.onNewIntent(intent)
}
fun getAppClass(name: String): Class<*> {
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}}
}
@@ -0,0 +1,35 @@
# Copyright 2020-2023 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
-keep class {{package-unescaped}}.* {
native <methods>;
}
-keep class {{package-unescaped}}.WryActivity {
public <init>(...);
void setWebView({{package-unescaped}}.RustWebView);
java.lang.Class getAppClass(...);
int getId();
java.lang.String getVersion();
int startActivity(...);
}
-keep class {{package-unescaped}}.Ipc {
public <init>(...);
@android.webkit.JavascriptInterface public <methods>;
}
-keep class {{package-unescaped}}.RustWebView {
public <init>(...);
void loadUrlMainThread(...);
void loadHTMLMainThread(...);
void evalScript(...);
}
-keep class {{package-unescaped}}.RustWebChromeClient,{{package-unescaped}}.RustWebViewClient {
public <init>(...);
}
+607
View File
@@ -0,0 +1,607 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{Error, InitializationScript, RGBA};
use crossbeam_channel::*;
use jni::{
errors::Result as JniResult,
objects::{GlobalRef, JMap, JObject, JString},
JNIEnv, JavaVM,
};
use once_cell::sync::Lazy;
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};
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 activity: GlobalRef,
pub window_manager: GlobalRef,
pub webview: Option<GlobalRef>,
pub webchrome_client: GlobalRef,
pub java_vm: *mut c_void,
}
unsafe impl Send for ActivityProxy {}
impl ActivityProxy {
pub fn new(
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<Mutex<BTreeMap<ActivityId, ActivityProxy>>> =
Lazy::new(|| Mutex::new(BTreeMap::new()));
pub fn activity_proxy(id: ActivityId) -> Option<ActivityProxy> {
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<ActivityId> {
for (activity_id, proxy) in ACTIVITY_PROXY.lock().unwrap().iter() {
let vm = unsafe { JavaVM::from_raw(proxy.java_vm.cast()) }.unwrap();
let mut env = vm.attach_current_thread_as_daemon().unwrap();
let equals = env
.call_method(
proxy.window_manager.as_obj(),
"equals",
"(Ljava/lang/Object;)Z",
&[(&window_manager).into()],
)
.and_then(|v| v.z())
.unwrap_or_default();
if equals {
return Some(*activity_id);
}
}
None
}
pub fn first_activity_id() -> Option<ActivityId> {
ACTIVITY_PROXY.lock().unwrap().keys().next().cloned()
}
pub fn get_webview(activity_id: ActivityId) -> Option<GlobalRef> {
ACTIVITY_PROXY
.lock()
.unwrap()
.get(&activity_id)
.unwrap()
.webview
.as_ref()
.cloned()
}
pub struct MainPipe<'a> {
pub env: JNIEnv<'a>,
}
impl<'a> MainPipe<'a> {
pub(crate) fn send(activity_id: ActivityId, message: WebViewMessage) {
let size = std::mem::size_of::<bool>();
if CHANNEL.0.send((activity_id, message)).is_ok() {
unsafe {
libc::write(
MAIN_PIPE[1].as_raw_fd(),
&true as *const _ as *const _,
size,
)
};
}
}
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,
#[cfg(any(debug_assertions, feature = "devtools"))]
devtools,
transparent,
background_color,
headers,
on_webview_created,
autoplay,
user_agent,
initialization_scripts,
id,
javascript_disabled,
..
} = attrs;
let string_class = self.env.find_class("java/lang/String")?;
let initialization_scripts_array = self.env.new_object_array(
initialization_scripts.len() as i32,
string_class,
self.env.new_string("")?,
)?;
for (i, init_script) in initialization_scripts.into_iter().enumerate() {
self.env.set_object_array_element(
&initialization_scripts_array,
i as i32,
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,
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(),
(&initialization_scripts_array).into(),
(&id).into(),
],
)?;
// get settings
let web_settings = self
.env
.call_method(
&webview,
"getSettings",
"()Landroid/webkit/WebSettings;",
&[],
)?
.l()?;
// set media autoplay
self.env.call_method(
&web_settings,
"setMediaPlaybackRequiresUserGesture",
"(Z)V",
&[(!autoplay).into()],
)?;
// set user-agent
if let Some(user_agent) = user_agent {
let user_agent = self.env.new_string(user_agent)?;
self.env.call_method(
&web_settings,
"setUserAgentString",
"(Ljava/lang/String;)V",
&[(&user_agent).into()],
)?;
}
// disable javascript
if javascript_disabled {
self.env.call_method(
&web_settings,
"setJavaScriptEnabled",
"(Z)V",
&[false.into()],
)?;
}
let webview_class_name = format!("{}/RustWebView", PACKAGE.get().unwrap());
self.env.call_method(
&activity,
"setWebView",
format!("(L{webview_class_name};)V"),
&[(&webview).into()],
)?;
// Navigation
if let Some(u) = url {
if let Ok(url) = self.env.new_string(u) {
load_url(&mut self.env, &webview, &url, headers, true)?;
}
} else if let Some(h) = html {
if let Ok(html) = self.env.new_string(h) {
load_html(&mut self.env, &webview, &html)?;
}
}
// Enable devtools
#[cfg(any(debug_assertions, feature = "devtools"))]
self.env.call_static_method(
&rust_webview_class,
"setWebContentsDebuggingEnabled",
"(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())?;
let webview_client = self.env.new_object(
&rust_webview_client_class,
format!("(L{webview_class_name};Landroid/content/Context;)V"),
&[(&webview).into(), (&activity).into()],
)?;
self.env.call_method(
&webview,
"setWebViewClient",
"(Landroid/webkit/WebViewClient;)V",
&[(&webview_client).into()],
)?;
// set webchrome client
self.env.call_method(
&webview,
"setWebChromeClient",
"(Landroid/webkit/WebChromeClient;)V",
&[web_chrome_client.as_obj().into()],
)?;
// Add javascript interface (IPC)
let ipc_class = find_class(
&mut self.env,
&activity,
format!("{}/Ipc", PACKAGE.get().unwrap()),
)?;
let ipc = self.env.new_object(
ipc_class,
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(
&webview,
"addJavascriptInterface",
"(Ljava/lang/Object;Ljava/lang/String;)V",
&[(&ipc).into(), (&ipc_str).into()],
)?;
// Set content view
self.env.call_method(
&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 {
env: &mut self.env,
activity: &activity,
webview: &webview,
}) {
#[cfg(feature = "tracing")]
tracing::warn!("failed to run webview created hook: {_e}");
}
}
let webview = self.env.new_global_ref(webview)?;
ACTIVITY_PROXY
.lock()
.unwrap()
.get_mut(&activity_id)
.unwrap()
.webview
.replace(webview);
}
WebViewMessage::Eval(script, callback) => {
if let Some(webview) = get_webview(activity_id) {
let id = EVAL_ID_GENERATOR.next() as i32;
#[cfg(feature = "tracing")]
let span = std::sync::Mutex::new(Some(SendEnteredSpan(
tracing::debug_span!("wry::eval").entered(),
)));
EVAL_CALLBACKS
.get_or_init(Default::default)
.lock()
.unwrap()
.insert(
id,
Box::new(move |result| {
#[cfg(feature = "tracing")]
span.lock().unwrap().take();
if let Some(callback) = &callback {
callback(result);
}
}),
);
let s = self.env.new_string(script)?;
self.env.call_method(
webview.as_obj(),
"evalScript",
"(ILjava/lang/String;)V",
&[id.into(), (&s).into()],
)?;
}
}
WebViewMessage::SetBackgroundColor(background_color) => {
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(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(),
}
} else {
tx.send(Err(Error::ActivityNotFound)).unwrap();
}
}
WebViewMessage::GetUrl(tx) => {
if let Some(webview) = get_webview(activity_id) {
let url = self
.env
.call_method(webview.as_obj(), "getUrl", "()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())
})
.unwrap_or_default();
tx.send(url).unwrap()
}
}
WebViewMessage::Jni(f) => {
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) = 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(activity_id) {
self
.env
.call_method(webview, "clearAllBrowsingData", "()V", &[])?;
}
}
WebViewMessage::LoadHtml(html) => {
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(activity_id) {
reload(&mut self.env, webview.as_obj())?;
}
}
WebViewMessage::GetCookies(tx, url) => {
if let Some(webview) = get_webview(activity_id) {
let url = self.env.new_string(url)?;
let cookies = self
.env
.call_method(
webview,
"getCookies",
"(Ljava/lang/String;)Ljava/lang/String;",
&[(&url).into()],
)
.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())
})
.unwrap_or_default();
tx.send(
cookies
.split("; ")
.flat_map(|c| cookie::Cookie::parse(c.to_string()))
.collect(),
)
.unwrap();
}
}
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(())
}
}
fn load_url<'a>(
env: &mut JNIEnv<'a>,
webview: &JObject<'a>,
url: &JString<'a>,
headers: Option<http::HeaderMap>,
main_thread: bool,
) -> JniResult<()> {
let function = if main_thread {
"loadUrlMainThread"
} else {
"loadUrl"
};
if let Some(headers) = headers {
let obj = env.new_object("java/util/HashMap", "()V", &[])?;
let headers_map = {
let headers_map = JMap::from_env(env, &obj)?;
for (name, value) in headers.iter() {
let key = env.new_string(name)?;
let value = env.new_string(value.to_str().unwrap_or_default())?;
headers_map.put(env, &key, &value)?;
}
headers_map
};
env.call_method(
webview,
function,
"(Ljava/lang/String;Ljava/util/Map;)V",
&[url.into(), (&headers_map).into()],
)?;
} else {
env.call_method(webview, function, "(Ljava/lang/String;)V", &[url.into()])?;
}
Ok(())
}
fn load_html<'a>(env: &mut JNIEnv<'a>, webview: &JObject<'a>, html: &JString<'a>) -> JniResult<()> {
env.call_method(
webview,
"loadHTMLMainThread",
"(Ljava/lang/String;)V",
&[html.into()],
)?;
Ok(())
}
fn reload<'a>(env: &mut JNIEnv<'a>, webview: &JObject<'a>) -> JniResult<()> {
env.call_method(webview, "reload", "()V", &[])?;
Ok(())
}
fn set_background_color<'a>(
env: &mut JNIEnv<'a>,
webview: &JObject<'a>,
(r, g, b, a): RGBA,
) -> JniResult<()> {
let color = (a as i32) << 24 | (r as i32) << 16 | (g as i32) << 8 | (b as i32);
env.call_method(webview, "setBackgroundColor", "(I)V", &[color.into()])?;
Ok(())
}
pub(crate) enum WebViewMessage {
CreateWebView(CreateWebViewAttributes),
Eval(String, Option<EvalCallback>),
SetBackgroundColor(RGBA),
GetWebViewVersion(Sender<Result<String, Error>>),
GetUrl(Sender<String>),
GetCookies(Sender<Vec<cookie::Cookie<'static>>>, String),
Jni(Box<dyn FnOnce(&mut JNIEnv, &JObject, &JObject) + Send>),
LoadUrl(String, Option<http::HeaderMap>),
LoadHtml(String),
Reload,
ClearAllBrowsingData,
OnDestroy {
activity_id: ActivityId,
webview_id: WebviewId,
is_changing_configurations: bool,
},
}
#[derive(Clone)]
pub(crate) struct CreateWebViewAttributes {
pub id: String,
pub url: Option<String>,
pub html: Option<String>,
#[cfg(any(debug_assertions, feature = "devtools"))]
pub devtools: bool,
pub transparent: bool,
pub background_color: Option<RGBA>,
pub headers: Option<http::HeaderMap>,
pub autoplay: bool,
pub on_webview_created:
Option<Arc<dyn Fn(super::Context) -> JniResult<()> + Send + Sync + 'static>>,
pub user_agent: Option<String>,
pub initialization_scripts: Vec<InitializationScript>,
pub javascript_disabled: bool,
}
// SAFETY: only use this when you are sure the span will be dropped on the same thread it was entered
#[cfg(feature = "tracing")]
struct SendEnteredSpan(tracing::span::EnteredSpan);
#[cfg(feature = "tracing")]
unsafe impl Send for SendEnteredSpan {}
+530
View File
@@ -0,0 +1,530 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use super::{PageLoadEvent, WebViewAttributes, RGBA};
use crate::{
custom_protocol_workaround, inject_initialization_scripts::inject_scripts_into_html, Error,
RequestAsyncResponder, Result,
};
use crossbeam_channel::*;
use http::{Request, Response as HttpResponse};
use jni::{
errors::Result as JniResult,
objects::{GlobalRef, JClass, JObject},
JNIEnv,
};
use ndk::looper::ThreadLooper;
use once_cell::sync::{Lazy, OnceCell};
use raw_window_handle::HasWindowHandle;
use std::{
borrow::Cow,
collections::HashMap,
sync::{mpsc::channel, Mutex},
time::Duration,
};
pub(crate) mod binding;
mod main_pipe;
use main_pipe::{
activity_id_for_window_manager, first_activity_id, register_activity_proxy, ActivityId,
CreateWebViewAttributes, MainPipe, WebViewMessage,
};
use crate::util::Counter;
static COUNTER: Counter = Counter::new();
const MAIN_PIPE_TIMEOUT: Duration = Duration::from_secs(10);
pub struct Context<'a, 'b> {
pub env: &'a mut JNIEnv<'b>,
pub activity: &'a JObject<'b>,
pub webview: &'a JObject<'b>,
}
type WebviewId = String;
macro_rules! define_static_handlers {
($($key: ident, $var:ident = $type_name:ident);+ $(;)?) => {
$(static $var: Lazy<Mutex<HashMap<$key, $type_name>>> = Lazy::new(||Mutex::new(HashMap::new()));)*
};
($($var:ident = $type_name:ident { $($fields:ident:$types:ty),+ $(,)? });+ $(;)?) => {
$(
static $var: Lazy<Mutex<HashMap<WebviewId, $type_name>>> = Lazy::new(||Mutex::new(HashMap::new()));
pub struct $type_name {
$($fields: $types,)*
}
impl $type_name {
pub fn new($($fields: $types,)*) -> Self {
Self {
$($fields,)*
}
}
}
unsafe impl Send for $type_name {}
unsafe impl Sync for $type_name {})*
};
}
define_static_handlers! {
IPC = UnsafeIpc { handler: Box<dyn Fn(Request<String>)> };
REQUEST_HANDLER = UnsafeRequestHandler { handler: Box<dyn Fn(&str, Request<Vec<u8>>, bool) -> Option<HttpResponse<Cow<'static, [u8]>>>> };
TITLE_CHANGE_HANDLER = UnsafeTitleHandler { handler: Box<dyn Fn(String)> };
URL_LOADING_OVERRIDE = UnsafeUrlLoadingOverride { handler: Box<dyn Fn(String) -> bool> };
ON_LOAD_HANDLER = UnsafeOnPageLoadHandler { handler: Box<dyn Fn(PageLoadEvent, String)> };
}
define_static_handlers! {
WebviewId, WITH_ASSET_LOADER = bool;
WebviewId, ASSET_LOADER_DOMAIN = String;
ActivityId, WEBVIEW_ATTRIBUTES = CreateWebViewAttributes;
}
pub(crate) static PACKAGE: OnceCell<String> = OnceCell::new();
type EvalCallback = Box<dyn Fn(String) + Send + 'static>;
pub static EVAL_ID_GENERATOR: Counter = Counter::new();
pub static EVAL_CALLBACKS: OnceCell<Mutex<HashMap<i32, EvalCallback>>> = 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,
/// hence the requirement for a [`ThreadLooper`].
pub unsafe fn android_setup(
package: &str,
mut env: JNIEnv,
_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(
&mut env,
activity.as_obj(),
format!("{}/RustWebChromeClient", PACKAGE.get().unwrap()),
)
.unwrap();
let webchrome_client = env
.new_object(
&rust_webchrome_client_class,
format!("(L{}/WryActivity;)V", PACKAGE.get().unwrap()),
&[activity.as_obj().into()],
)
.unwrap();
let webchrome_client = env.new_global_ref(webchrome_client).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,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
) -> Result<Self> {
Self::new(window, attributes, pl_attrs)
}
pub fn new(
window: &impl HasWindowHandle,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
) -> Result<Self> {
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,
initialization_scripts,
ipc_handler,
#[cfg(any(debug_assertions, feature = "devtools"))]
devtools,
custom_protocols,
background_color,
transparent,
headers,
autoplay,
user_agent,
javascript_disabled,
..
} = attributes;
let super::PlatformSpecificWebViewAttributes {
on_webview_created,
with_asset_loader,
asset_loader_domain,
https_scheme,
} = pl_attrs;
let http_or_https = if https_scheme { "https" } else { "http" };
let url = if let Some(mut url) = url {
if let Some((protocol, _)) = url.split_once("://") {
if custom_protocols.contains_key(protocol) {
url = custom_protocol_workaround::apply_uri_work_around(&url, http_or_https, protocol)
}
}
Some(url)
} else {
None
};
let id = attributes
.id
.map(|id| id.to_string())
.unwrap_or_else(|| COUNTER.next().to_string());
WITH_ASSET_LOADER
.lock()
.unwrap()
.insert(id.clone(), with_asset_loader);
if let Some(domain) = asset_loader_domain {
ASSET_LOADER_DOMAIN
.lock()
.unwrap()
.insert(id.clone(), domain);
}
let initialization_scripts_ = initialization_scripts.clone();
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, 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<dyn FnOnce(HttpResponse<Cow<'static, [u8]>>)> =
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 });
// 3x the timeout while we monitor https://github.com/tauri-apps/wry/issues/1551
// TODO: Remove timeout
return rx.recv_timeout(MAIN_PIPE_TIMEOUT * 3).inspect_err(|e| {eprintln!("custom protocol timed out: {e}");}).ok();
}
None
},
)));
if let Some(i) = ipc_handler {
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()
.insert(id.clone(), UnsafeTitleHandler::new(i));
}
if let Some(i) = attributes.navigation_handler {
URL_LOADING_OVERRIDE
.lock()
.unwrap()
.insert(id.clone(), UnsafeUrlLoadingOverride::new(i));
}
if let Some(h) = attributes.on_page_load_handler {
ON_LOAD_HANDLER
.lock()
.unwrap()
.insert(id.clone(), UnsafeOnPageLoadHandler::new(h));
}
let attributes = CreateWebViewAttributes {
id: id.clone(),
url,
html,
#[cfg(any(debug_assertions, feature = "devtools"))]
devtools,
background_color,
transparent,
headers,
on_webview_created,
autoplay,
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, activity_id })
}
pub fn print(&self) -> crate::Result<()> {
Ok(())
}
pub fn id(&self) -> crate::WebViewId<'_> {
&self.id
}
pub fn url(&self) -> crate::Result<String> {
let (tx, rx) = bounded(1);
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<impl Fn(String) + Send + 'static>) -> Result<()> {
MainPipe::send(
self.activity_id,
WebViewMessage::Eval(
js.into(),
callback.map(|c| Box::new(c) as Box<dyn Fn(String) + Send + 'static>),
),
);
Ok(())
}
#[cfg(any(debug_assertions, feature = "devtools"))]
pub fn open_devtools(&self) {}
#[cfg(any(debug_assertions, feature = "devtools"))]
pub fn close_devtools(&self) {}
#[cfg(any(debug_assertions, feature = "devtools"))]
pub fn is_devtools_open(&self) -> bool {
false
}
pub fn zoom(&self, _scale_factor: f64) -> Result<()> {
Ok(())
}
pub fn set_background_color(&self, background_color: RGBA) -> Result<()> {
MainPipe::send(
self.activity_id,
WebViewMessage::SetBackgroundColor(background_color),
);
Ok(())
}
pub fn load_url(&self, url: &str) -> Result<()> {
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(
self.activity_id,
WebViewMessage::LoadUrl(url.to_string(), Some(headers)),
);
Ok(())
}
pub fn load_html(&self, html: &str) -> Result<()> {
MainPipe::send(self.activity_id, WebViewMessage::LoadHtml(html.to_string()));
Ok(())
}
pub fn reload(&self) -> Result<()> {
MainPipe::send(self.activity_id, WebViewMessage::Reload);
Ok(())
}
pub fn clear_all_browsing_data(&self) -> Result<()> {
MainPipe::send(self.activity_id, WebViewMessage::ClearAllBrowsingData);
Ok(())
}
pub fn cookies_for_url(&self, url: &str) -> Result<Vec<cookie::Cookie<'static>>> {
let (tx, rx) = bounded(1);
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, #[allow(unused)] cookie: &cookie::Cookie<'_>) -> Result<()> {
// Unsupported
Ok(())
}
pub fn delete_cookie(&self, #[allow(unused)] cookie: &cookie::Cookie<'_>) -> Result<()> {
// Unsupported
Ok(())
}
pub fn cookies(&self) -> Result<Vec<cookie::Cookie<'static>>> {
Ok(Vec::new())
}
pub fn bounds(&self) -> Result<crate::Rect> {
Ok(crate::Rect::default())
}
pub fn set_bounds(&self, _bounds: crate::Rect) -> Result<()> {
// Unsupported
Ok(())
}
pub fn set_visible(&self, _visible: bool) -> Result<()> {
// Unsupported
Ok(())
}
pub fn focus(&self) -> Result<()> {
// Unsupported
Ok(())
}
pub fn focus_parent(&self) -> Result<()> {
// Unsupported
Ok(())
}
}
#[derive(Clone, Copy)]
pub struct JniHandle {
pub(crate) activity_id: ActivityId,
}
impl JniHandle {
/// Execute jni code on the thread of the webview.
/// Provided function will be provided with the jni evironment, Android activity and WebView
pub fn exec<F>(&self, func: F)
where
F: FnOnce(&mut JNIEnv, &JObject, &JObject) + Send + 'static,
{
MainPipe::send(self.activity_id, WebViewMessage::Jni(Box::new(func)));
}
}
pub fn platform_webview_version() -> Result<String> {
let (tx, rx) = bounded(1);
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)?
}
/// Finds a class in the project scope.
pub fn find_class<'a>(
env: &mut JNIEnv<'a>,
activity: &JObject<'_>,
name: String,
) -> JniResult<JClass<'a>> {
let class_name = env.new_string(name.replace('/', "."))?;
let my_class = env
.call_method(
activity,
"getAppClass",
"(Ljava/lang/String;)Ljava/lang/Class;",
&[(&class_name).into()],
)?
.l()?;
Ok(my_class.into())
}
/// Dispatch a closure to run on the Android context.
///
/// The closure takes the JNI env, the Android activity instance and the possibly null webview.
pub fn dispatch<F>(func: F)
where
F: FnOnce(&mut JNIEnv, &JObject, &JObject) + Send + 'static,
{
MainPipe::send(
first_activity_id().expect("no available activity"),
WebViewMessage::Jni(Box::new(func)),
);
}