diff --git a/.changes/android-custom-protocol.md b/.changes/android-custom-protocol.md new file mode 100644 index 000000000..7c181bf10 --- /dev/null +++ b/.changes/android-custom-protocol.md @@ -0,0 +1,5 @@ +--- +"tao": patch +--- + +Implement custom protocol on Android. diff --git a/Cargo.toml b/Cargo.toml index 873c82877..ead0c4559 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ ndk-sys = "0.3" ndk-context = "0.1" once_cell = "1.13" paste = "1.0" +http = "0.2" [target."cfg(any(target_os = \"ios\", target_os = \"macos\"))".dependencies] objc = "0.2" diff --git a/src/platform_impl/android/ndk_glue.rs b/src/platform_impl/android/ndk_glue.rs index 422886d60..dad12f56f 100644 --- a/src/platform_impl/android/ndk_glue.rs +++ b/src/platform_impl/android/ndk_glue.rs @@ -1,7 +1,12 @@ use crate::window::Window; use crossbeam_channel::*; +use http::{ + header::{HeaderMap, HeaderName, HeaderValue}, + status::StatusCode, +}; pub use jni::{ - objects::{GlobalRef, JClass, JObject, JString}, + objects::{GlobalRef, JClass, JMap, JObject, JString}, + sys::jobject, JNIEnv, }; use libc::c_void; @@ -46,6 +51,7 @@ macro_rules! android_fn { android_fn!($domain, $package, MainActivity, memory); android_fn!($domain, $package, MainActivity, focus, i32); android_fn!($domain, $package, RustClient, eval); + android_fn!($domain, $package, RustClient, handleRequest, JObject, jobject); android_fn!($domain, $package, IpcInterface, ipc, JString); } }; @@ -53,13 +59,16 @@ macro_rules! android_fn { android_fn!($domain, $package, $class, $function, JObject) }; ($domain:ident, $package:ident, $class:ident, $function:ident, $arg:ty) => { + android_fn!($domain, $package, $class, $function, $arg, ()) + }; + ($domain:ident, $package:ident, $class:ident, $function:ident, $arg:ty, $ret: ty) => { paste::paste! { #[no_mangle] unsafe extern "C" fn [< Java_ $domain _ $package _ $class _ $function >]( env: JNIEnv, class: JClass, object: $arg, - ) { + ) -> $ret { $function(env, class, object) } } @@ -188,6 +197,8 @@ pub enum WebViewMessage { } pub static IPC: OnceCell = OnceCell::new(); +pub static REQUEST_HANDLER: OnceCell = OnceCell::new(); + pub struct UnsafeIpc(*mut c_void, Rc); impl UnsafeIpc { pub fn new(f: *mut c_void, w: Rc) -> Self { @@ -197,6 +208,15 @@ impl UnsafeIpc { 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 {} + /// `ndk-glue` macros register the reading end of an event pipe with the /// main [`ThreadLooper`] under this `ident`. /// When returned from [`ThreadLooper::poll_*`](ThreadLooper::poll_once) @@ -398,6 +418,123 @@ pub unsafe fn eval(_: JNIEnv, _: JClass, _: JObject) { MainPipe::send(WebViewMessage::Eval); } +pub struct WebResourceRequest { + /// The request url. + pub url: String, + /// The request method. + pub method: String, + /// The request headers. + pub headers: HeaderMap, +} + +pub struct WebResourceResponse { + /// The response's status + pub status: StatusCode, + + /// The response's headers + pub headers: HeaderMap, + + /// The response's mimetype type + pub mimetype: Option, + + /// The response body. + pub body: Vec, +} + +fn handle_request(env: JNIEnv, request: JObject) -> Result { + 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(); + + let method: JString = env + .call_method(request, "getMethod", "()Ljava/lang/String;", &[])? + .l()? + .into(); + let method = env.get_string(method)?.to_string_lossy().to_string(); + + let request_headers = env + .call_method(request, "getRequestHeaders", "()Ljava/util/Map;", &[])? + .l()?; + let request_headers = JMap::from_env(&env, request_headers)?; + let mut headers = HeaderMap::new(); + 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()), + ) { + headers.insert(header, value); + } + } + + if let Some(handler) = REQUEST_HANDLER.get() { + let response = (handler.0)(WebResourceRequest { + url, + method, + headers, + }); + 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 bytes = response.body; + + 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 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) => {