diff --git a/.changes/refactor-android.md b/.changes/refactor-android.md new file mode 100644 index 000000000..b6f1da89d --- /dev/null +++ b/.changes/refactor-android.md @@ -0,0 +1,5 @@ +--- +"wry": patch +--- + +Move WebView logic from tao to wry. diff --git a/Cargo.toml b/Cargo.toml index 50c0bb676..278469558 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" thiserror = "1.0" url = "2.2" -tao = { git = "https://github.com/tauri-apps/tao", branch = "dev", default-features = false, features = [ "serde" ] } +tao = { version = "0.13.3", default-features = false, features = [ "serde" ] } http = "0.2.8" [dev-dependencies] @@ -85,5 +85,5 @@ core-graphics = "0.22" objc = "0.2" objc_id = "0.1" -[target."cfg(target_os = \"android\")"] -dependencies = { } +[target."cfg(target_os = \"android\")".dependencies] +crossbeam-channel = "0.5" diff --git a/src/http/mod.rs b/src/http/mod.rs index d999158fd..ae843e65a 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -2,8 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -#![allow(unused_imports, dead_code)] - // custom wry types mod request; mod response; diff --git a/src/http/request.rs b/src/http/request.rs index 980a9ec83..950ad38d6 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -70,6 +70,12 @@ impl Request { &self.head.uri } + /// Returns a mutable reference to the associated URI. + #[inline] + pub fn uri_mut(&mut self) -> &mut String { + &mut self.head.uri + } + /// Returns a reference to the associated header field map. #[inline] pub fn headers(&self) -> &HeaderMap { diff --git a/src/webview/android/binding.rs b/src/webview/android/binding.rs new file mode 100644 index 000000000..928572ed2 --- /dev/null +++ b/src/webview/android/binding.rs @@ -0,0 +1,127 @@ +use crate::http::{ + header::{HeaderName, HeaderValue}, + RequestBuilder, +}; +use tao::platform::android::ndk_glue::jni::{ + errors::Error as JniError, + objects::{JClass, JMap, JObject, JString}, + sys::jobject, + JNIEnv, +}; + +use super::{MainPipe, WebViewMessage, IPC, REQUEST_HANDLER}; + +#[allow(non_snake_case)] +pub unsafe fn runInitializationScripts(_: JNIEnv, _: JClass, _: JObject) { + MainPipe::send(WebViewMessage::RunInitializationScripts); +} + +fn handle_request(env: JNIEnv, request: JObject) -> Result { + let mut request_builder = RequestBuilder::new(); + + let uri = env + .call_method(request, "getUrl", "()Landroid/net/Uri;", &[])? + .l()?; + let url: JString = env + .call_method(uri, "toString", "()Ljava/lang/String;", &[])? + .l()? + .into(); + request_builder = request_builder.uri(&env.get_string(url)?.to_string_lossy().to_string()); + + let method: JString = env + .call_method(request, "getMethod", "()Ljava/lang/String;", &[])? + .l()? + .into(); + 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)?; + for (header, value) in request_headers.iter()? { + let header = env.get_string(header.into())?; + let value = env.get_string(value.into())?; + 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); + } + } + + if let Some(handler) = REQUEST_HANDLER.get() { + let response = (handler.0)(request_builder.body(Vec::new()).unwrap()); + if let Some(response) = response { + let status_code = response.status().as_u16() as i32; + let reason_phrase = "OK"; + let encoding = "UTF-8"; + let mime_type = if let Some(mime) = response.mimetype() { + env.new_string(mime)?.into() + } else { + JObject::null() + }; + + let hashmap = env.find_class("java/util/HashMap")?; + let response_headers = env.new_object(hashmap, "()V", &[])?; + for (key, value) in response.headers().iter() { + env.call_method( + response_headers, + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + &[ + env.new_string(key.as_str())?.into(), + // TODO can we handle this better? + env + .new_string(String::from_utf8_lossy(value.as_bytes()))? + .into(), + ], + )?; + } + + 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 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(), env.new_string(encoding)?.into(), status_code.into(), env.new_string(reason_phrase)?.into(), response_headers.into(), stream.into()], + )?; + + return Ok(*web_resource_response); + } + } + Ok(*JObject::null()) +} + +#[allow(non_snake_case)] +pub unsafe fn handleRequest(env: JNIEnv, _: JClass, request: JObject) -> jobject { + match handle_request(env, request) { + Ok(response) => response, + Err(e) => { + log::error!("Failed to handle request: {}", e); + *JObject::null() + } + } +} + +pub unsafe fn ipc(env: JNIEnv, _: JClass, arg: JString) { + match env.get_string(arg) { + Ok(arg) => { + let arg = arg.to_string_lossy().to_string(); + if let Some(w) = IPC.get() { + (w.0)(&w.1, arg) + } + } + Err(e) => log::error!("Failed to parse JString: {}", e), + } +} diff --git a/src/webview/android/main_pipe.rs b/src/webview/android/main_pipe.rs new file mode 100644 index 000000000..0f42c8f86 --- /dev/null +++ b/src/webview/android/main_pipe.rs @@ -0,0 +1,181 @@ +use crossbeam_channel::*; +use once_cell::sync::Lazy; +use std::os::unix::prelude::*; +use tao::platform::android::ndk_glue::{ + jni::{ + errors::Error as JniError, + objects::{GlobalRef, JClass, JObject}, + JNIEnv, + }, + PACKAGE, +}; + +static CHANNEL: Lazy<(Sender, Receiver)> = Lazy::new(|| bounded(8)); +pub static MAIN_PIPE: Lazy<[RawFd; 2]> = Lazy::new(|| { + let mut pipe: [RawFd; 2] = Default::default(); + unsafe { libc::pipe(pipe.as_mut_ptr()) }; + pipe +}); + +pub struct MainPipe<'a> { + pub env: JNIEnv<'a>, + pub activity: GlobalRef, + pub initialization_scripts: Vec, + pub webview: Option, +} + +impl MainPipe<'_> { + pub fn send(message: WebViewMessage) { + let size = std::mem::size_of::(); + if let Ok(()) = CHANNEL.0.send(message) { + unsafe { libc::write(MAIN_PIPE[1], &true as *const _ as *const _, size) }; + } + } + + pub fn recv(&mut self) -> Result<(), JniError> { + let env = self.env; + let activity = self.activity.as_obj(); + if let Ok(message) = CHANNEL.1.recv() { + match message { + WebViewMessage::CreateWebView(url, mut initialization_scripts, devtools) => { + // Create webview + let class = env.find_class("android/webkit/WebView")?; + let webview = + env.new_object(class, "(Landroid/content/Context;)V", &[activity.into()])?; + + // Enable Javascript + let settings = env + .call_method( + webview, + "getSettings", + "()Landroid/webkit/WebSettings;", + &[], + )? + .l()?; + env.call_method(settings, "setJavaScriptEnabled", "(Z)V", &[true.into()])?; + + // Load URL + if let Ok(url) = env.new_string(url) { + env.call_method(webview, "loadUrl", "(Ljava/lang/String;)V", &[url.into()])?; + } + + // Enable devtools + env.call_static_method( + class, + "setWebContentsDebuggingEnabled", + "(Z)V", + &[devtools.into()], + )?; + + // Initialize scripts + self + .initialization_scripts + .append(&mut initialization_scripts); + + // Create and set webview client + println!( + "[RUST] webview client {}/RustWebViewClient", + PACKAGE.get().unwrap() + ); + let rust_webview_client_class = find_my_class( + env, + activity, + format!("{}/RustWebViewClient", PACKAGE.get().unwrap()), + )?; + let webview_client = env.new_object(rust_webview_client_class, "()V", &[])?; + env.call_method( + webview, + "setWebViewClient", + "(Landroid/webkit/WebViewClient;)V", + &[webview_client.into()], + )?; + + // Create and set webchrome client + println!("[RUST] chrome client"); + let rust_webchrome_client_class = find_my_class( + env, + activity, + format!("{}/RustWebChromeClient", PACKAGE.get().unwrap()), + )?; + let webchrome_client = env.new_object(rust_webchrome_client_class, "()V", &[])?; + env.call_method( + webview, + "setWebChromeClient", + "(Landroid/webkit/WebChromeClient;)V", + &[webchrome_client.into()], + )?; + + // Add javascript interface (IPC) + let ipc_class = find_my_class(env, activity, format!("{}/Ipc", PACKAGE.get().unwrap()))?; + let ipc = env.new_object(ipc_class, "()V", &[])?; + let ipc_str = env.new_string("ipc")?; + env.call_method( + webview, + "addJavascriptInterface", + "(Ljava/lang/Object;Ljava/lang/String;)V", + &[ipc.into(), ipc_str.into()], + )?; + + // Set content view + env.call_method( + activity, + "setContentView", + "(Landroid/view/View;)V", + &[webview.into()], + )?; + let webview = env.new_global_ref(webview)?; + self.webview = Some(webview); + } + WebViewMessage::RunInitializationScripts => { + if let Some(webview) = &self.webview { + for s in &self.initialization_scripts { + let s = env.new_string(s)?; + env.call_method( + webview.as_obj(), + "evaluateJavascript", + "(Ljava/lang/String;Landroid/webkit/ValueCallback;)V", + &[s.into(), JObject::null().into()], + )?; + } + } + } + WebViewMessage::Eval(script) => { + if let Some(webview) = &self.webview { + let s = env.new_string(script)?; + env.call_method( + webview.as_obj(), + "evaluateJavascript", + "(Ljava/lang/String;Landroid/webkit/ValueCallback;)V", + &[s.into(), JObject::null().into()], + )?; + } + } + } + } + Ok(()) + } +} + +fn find_my_class<'a>( + env: JNIEnv<'a>, + activity: JObject<'a>, + name: String, +) -> Result, JniError> { + 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()) +} + +#[derive(Debug)] +pub enum WebViewMessage { + CreateWebView(String, Vec, bool), + RunInitializationScripts, + Eval(String), +} diff --git a/src/webview/android/mod.rs b/src/webview/android/mod.rs index c6cdbfe66..0afa3ba0d 100644 --- a/src/webview/android/mod.rs +++ b/src/webview/android/mod.rs @@ -1,11 +1,95 @@ use super::{WebContext, WebViewAttributes}; use crate::{ application::window::Window, - http::{method::Method, Request as HttpRequest, RequestParts}, + http::{Request as HttpRequest, Response as HttpResponse}, Result, }; -use std::{rc::Rc, str::FromStr}; -use tao::platform::android::ndk_glue::*; +use once_cell::sync::OnceCell; +use std::rc::Rc; +use tao::platform::android::ndk_glue::{ + jni::{objects::GlobalRef, JNIEnv}, + ndk::looper::{FdEvent, ForeignLooper}, +}; + +pub(crate) mod binding; +mod main_pipe; +use main_pipe::{MainPipe, WebViewMessage, MAIN_PIPE}; + +#[macro_export] +macro_rules! android_binding { + ($domain:ident, $package:ident, $main: ident) => { + android_binding!($domain, $package, $main, ::wry) + }; + ($domain:ident, $package:ident, $main: ident, $wry: path) => { + use $wry::{ + application::{ + android_binding as tao_android_binding, android_fn, platform::android::ndk_glue::*, + }, + webview::prelude::*, + }; + tao_android_binding!($domain, $package, setup, $main); + android_fn!( + $domain, + $package, + RustWebChromeClient, + runInitializationScripts + ); + android_fn!( + $domain, + $package, + RustWebViewClient, + handleRequest, + JObject, + jobject + ); + android_fn!($domain, $package, Ipc, ipc, JString); + }; +} + +pub static IPC: OnceCell = OnceCell::new(); +pub static REQUEST_HANDLER: OnceCell = OnceCell::new(); + +pub struct UnsafeIpc(Box, Rc); +impl UnsafeIpc { + pub fn new(f: Box, w: Rc) -> Self { + Self(f, w) + } +} +unsafe impl Send for UnsafeIpc {} +unsafe impl Sync for UnsafeIpc {} + +pub struct UnsafeRequestHandler(Box Option>); +impl UnsafeRequestHandler { + pub fn new(f: Box Option>) -> Self { + Self(f) + } +} +unsafe impl Send for UnsafeRequestHandler {} +unsafe impl Sync for UnsafeRequestHandler {} + +pub unsafe fn setup(env: JNIEnv, looper: &ForeignLooper, activity: GlobalRef) { + let mut main_pipe = MainPipe { + env, + activity, + initialization_scripts: vec![], + webview: None, + }; + + looper + .add_fd_with_callback(MAIN_PIPE[0], FdEvent::INPUT, move |_| { + let size = std::mem::size_of::(); + let mut wake = false; + if libc::read(MAIN_PIPE[0], &mut wake as *mut _ as *mut _, size) == size as libc::ssize_t { + match main_pipe.recv() { + Ok(_) => true, + Err(_) => false, + } + } else { + false + } + }) + .unwrap(); +} pub struct InnerWebView { pub window: Rc, @@ -47,32 +131,18 @@ impl InnerWebView { } REQUEST_HANDLER.get_or_init(move || { - UnsafeRequestHandler::new(Box::new(move |request: WebResourceRequest| { + UnsafeRequestHandler::new(Box::new(move |mut request| { if let Some(custom_protocol) = custom_protocols .iter() - .find(|(name, _)| request.url.starts_with(&format!("https://{}.", name))) + .find(|(name, _)| request.uri().starts_with(&format!("https://{}.", name))) { - let path = request.url.replace( + *request.uri_mut() = request.uri().replace( &format!("https://{}.", custom_protocol.0), &format!("{}://", custom_protocol.0), ); - let request = HttpRequest { - head: RequestParts { - method: Method::from_str(&request.method).unwrap_or(Method::GET), - uri: path, - headers: request.headers, - }, - body: Vec::new(), - }; - if let Ok(response) = (custom_protocol.1)(&request) { - return Some(WebResourceResponse { - status: response.head.status, - headers: response.head.headers, - mimetype: response.head.mimetype, - body: response.body, - }); + return Some(response); } } @@ -82,7 +152,7 @@ impl InnerWebView { let w = window.clone(); if let Some(i) = ipc_handler { - IPC.get_or_init(move || UnsafeIpc::new(Box::into_raw(Box::new(i)) as *mut _, w)); + IPC.get_or_init(move || UnsafeIpc::new(Box::new(i), w)); } Ok(Self { window }) diff --git a/src/webview/mod.rs b/src/webview/mod.rs index 1bdb20965..cf959b1d6 100644 --- a/src/webview/mod.rs +++ b/src/webview/mod.rs @@ -11,6 +11,10 @@ pub use web_context::WebContext; #[cfg(target_os = "android")] pub(crate) mod android; #[cfg(target_os = "android")] +pub mod prelude { + pub use super::android::{binding::*, setup}; +} +#[cfg(target_os = "android")] use android::*; #[cfg(any( target_os = "linux",