Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(android): move WebView logic from tao to wry #659

Merged
merged 4 commits into from
Aug 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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