Skip to content

Commit

Permalink
refactor(android): move WebView logic from tao to wry (#659)
Browse files Browse the repository at this point in the history
* refactor(android): move WebView logic from tao to wry

* refactor: use http types

* strong type UnsafeIpc

* pin tao
  • Loading branch information
lucasfernog authored Aug 14, 2022
1 parent 690fd26 commit aba1ae5
Show file tree
Hide file tree
Showing 8 changed files with 418 additions and 27 deletions.
5 changes: 5 additions & 0 deletions .changes/refactor-android.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wry": patch
---

Move WebView logic from tao to wry.
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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"
2 changes: 0 additions & 2 deletions src/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/http/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HeaderValue> {
Expand Down
127 changes: 127 additions & 0 deletions src/webview/android/binding.rs
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),
}
}
181 changes: 181 additions & 0 deletions src/webview/android/main_pipe.rs
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),
}
Loading

0 comments on commit aba1ae5

Please sign in to comment.