Skip to content

Commit

Permalink
feat(android): implement custom protocol (#527)
Browse files Browse the repository at this point in the history
* feat(android): implement custom protocol

* add change file

* fix status code, use log::error

* remove println
  • Loading branch information
lucasfernog committed Aug 13, 2022
1 parent 06949a7 commit b464b8a
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changes/android-custom-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tao": patch
---

Implement custom protocol on Android.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
141 changes: 139 additions & 2 deletions src/platform_impl/android/ndk_glue.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -46,20 +51,24 @@ 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);
}
};
($domain:ident, $package:ident, $class:ident, $function:ident) => {
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)
}
}
Expand Down Expand Up @@ -188,6 +197,8 @@ pub enum WebViewMessage {
}

pub static IPC: OnceCell<UnsafeIpc> = OnceCell::new();
pub static REQUEST_HANDLER: OnceCell<UnsafeRequestHandler> = OnceCell::new();

pub struct UnsafeIpc(*mut c_void, Rc<Window>);
impl UnsafeIpc {
pub fn new(f: *mut c_void, w: Rc<Window>) -> Self {
Expand All @@ -197,6 +208,15 @@ impl UnsafeIpc {
unsafe impl Send for UnsafeIpc {}
unsafe impl Sync for UnsafeIpc {}

pub struct UnsafeRequestHandler(Box<dyn Fn(WebResourceRequest) -> Option<WebResourceResponse>>);
impl UnsafeRequestHandler {
pub fn new(f: Box<dyn Fn(WebResourceRequest) -> Option<WebResourceResponse>>) -> 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)
Expand Down Expand Up @@ -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<HeaderValue>,
}

pub struct WebResourceResponse {
/// The response's status
pub status: StatusCode,

/// The response's headers
pub headers: HeaderMap<HeaderValue>,

/// The response's mimetype type
pub mimetype: Option<String>,

/// The response body.
pub body: Vec<u8>,
}

fn handle_request(env: JNIEnv, request: JObject) -> Result<jobject, jni::errors::Error> {
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) => {
Expand Down

0 comments on commit b464b8a

Please sign in to comment.