feat: 大屏桌面壳(Tauri 2)
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user