484 lines
14 KiB
Rust
484 lines
14 KiB
Rust
|
|
// 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}")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|