-
-
Notifications
You must be signed in to change notification settings - Fork 302
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(android): move WebView logic from tao to wry (#659)
* refactor(android): move WebView logic from tao to wry * refactor: use http types * strong type UnsafeIpc * pin tao
- Loading branch information
1 parent
690fd26
commit aba1ae5
Showing
8 changed files
with
418 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"wry": patch | ||
--- | ||
|
||
Move WebView logic from tao to wry. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<jobject, JniError> { | ||
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), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<WebViewMessage>, Receiver<WebViewMessage>)> = 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<String>, | ||
pub webview: Option<GlobalRef>, | ||
} | ||
|
||
impl MainPipe<'_> { | ||
pub fn send(message: WebViewMessage) { | ||
let size = std::mem::size_of::<bool>(); | ||
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<JClass<'a>, 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<String>, bool), | ||
RunInitializationScripts, | ||
Eval(String), | ||
} |
Oops, something went wrong.