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:
+483
@@ -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
@@ -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}}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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}}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user