extern crate alloc;
extern crate std;
use alloc::boxed::Box;
use alloc::rc::Rc;
use alloc::string::String;
use alloc::string::ToString;
use jni::objects::{Global, JObject, JString, JValue};
use jni::sys::{jboolean, jfloat, jint, jlong, jobject};
use jni::{Env, EnvUnowned, JavaVM, jni_sig, jni_str};
use crate::closure::WuiFn;
use crate::components::webview::{
FfiWebViewHandle, WuiJsCallback, WuiJsReplyKind, WuiScriptInjectionTime, WuiStringCallback,
WuiWebViewEvent, WuiWebViewEventType, WuiWebViewHandle, WuiWebViewMessage, WuiWebViewReply,
};
use crate::reactive::WuiComputed;
use crate::{IntoFFI, IntoRust, WuiStr};
use base64::Engine as _;
use waterui_webview::{CustomWebViewController, WebViewController, WebViewHandle, bridge};
use std::{collections::HashMap, sync::Arc};
struct AndroidWebViewHandle {
jvm: Arc<JavaVM>,
wrapper: Global<JObject<'static>>,
event_callback: Option<Rc<WuiFn<WuiWebViewEvent>>>,
handlers: HashMap<String, Rc<WuiFn<WuiWebViewMessage>>>,
}
impl AndroidWebViewHandle {
fn with_env<R>(&self, f: impl FnOnce(&mut Env) -> R) -> R {
super::with_attached_env(&self.jvm, f)
.expect("AndroidWebViewHandle failed to attach its JVM thread")
}
}
struct AndroidWebViewFactory {
jvm: Arc<JavaVM>,
factory: Global<JObject<'static>>,
}
impl AndroidWebViewFactory {
fn with_env<R>(&self, f: impl FnOnce(&mut Env) -> R) -> R {
super::with_attached_env(&self.jvm, f)
.expect("AndroidWebViewFactory failed to attach its JVM thread")
}
fn create_webview(&self) -> WuiWebViewHandle {
self.with_env(|env| {
let wrapper_obj = env
.call_method(
&self.factory,
jni_str!("create"),
jni_sig!("()Ldev/waterui/android/components/WebViewWrapper;"),
&[],
)
.expect("Android WebViewFactory.create failed")
.l()
.expect("Android WebViewFactory.create did not return a WebViewWrapper");
let wrapper_ref = env
.new_global_ref(wrapper_obj)
.expect("Failed to retain WebViewWrapper");
let handle = Box::new(AndroidWebViewHandle {
jvm: Arc::clone(&self.jvm),
wrapper: wrapper_ref,
event_callback: None,
handlers: HashMap::new(),
});
let handle_ptr = Box::into_raw(handle) as *mut ();
WuiWebViewHandle {
data: handle_ptr,
go_back: webview_go_back,
go_forward: webview_go_forward,
go_to: webview_go_to,
stop: webview_stop,
refresh: webview_refresh,
can_go_back: webview_can_go_back,
can_go_forward: webview_can_go_forward,
set_user_agent: webview_set_user_agent,
set_redirects_enabled: webview_set_redirects_enabled,
inject_script: webview_inject_script,
watch: webview_watch,
add_handler: Some(webview_add_handler),
remove_handler: Some(webview_remove_handler),
set_bridge_origins: Some(webview_set_bridge_origins),
set_cookie: Some(webview_set_cookie),
get_cookies: Some(webview_get_cookies),
run_javascript: webview_run_javascript,
call_async_javascript: webview_call_async_javascript,
drop: webview_drop,
}
})
}
}
impl CustomWebViewController for AndroidWebViewFactory {
fn open(&self) -> impl WebViewHandle {
FfiWebViewHandle::new(self.create_webview())
}
}
fn wui_str_to_string(s: WuiStr) -> String {
let s: waterui::Str = unsafe { s.into_rust() };
s.as_str().to_string()
}
fn java_string<'local>(env: &mut Env<'local>, s: &str) -> JString<'local> {
env.new_string(s).expect("Failed to create Java string")
}
unsafe extern "C" fn webview_go_back(data: *mut ()) {
let handle = unsafe { &*(data as *const AndroidWebViewHandle) };
handle.with_env(|env| {
env.call_method(&handle.wrapper, jni_str!("goBack"), jni_sig!("()V"), &[])
.expect("webview_go_back: failed to call WebViewWrapper.goBack()");
});
}
unsafe extern "C" fn webview_go_forward(data: *mut ()) {
let handle = unsafe { &*(data as *const AndroidWebViewHandle) };
handle.with_env(|env| {
env.call_method(&handle.wrapper, jni_str!("goForward"), jni_sig!("()V"), &[])
.expect("webview_go_forward: failed to call WebViewWrapper.goForward()");
});
}
unsafe extern "C" fn webview_go_to(data: *mut (), url: WuiStr) {
let handle = unsafe { &*(data as *const AndroidWebViewHandle) };
let url = wui_str_to_string(url);
handle.with_env(|env| {
let jurl = java_string(env, &url);
env.call_method(
&handle.wrapper,
jni_str!("goTo"),
jni_sig!("(Ljava/lang/String;)V"),
&[JValue::Object(&jurl)],
)
.expect("webview_go_to: failed to call WebViewWrapper.goTo(String)");
});
}
unsafe extern "C" fn webview_stop(data: *mut ()) {
let handle = unsafe { &*(data as *const AndroidWebViewHandle) };
handle.with_env(|env| {
env.call_method(&handle.wrapper, jni_str!("stop"), jni_sig!("()V"), &[])
.expect("webview_stop: failed to call WebViewWrapper.stop()");
});
}
unsafe extern "C" fn webview_refresh(data: *mut ()) {
let handle = unsafe { &*(data as *const AndroidWebViewHandle) };
handle.with_env(|env| {
env.call_method(&handle.wrapper, jni_str!("refresh"), jni_sig!("()V"), &[])
.expect("webview_refresh: failed to call WebViewWrapper.refresh()");
});
}
unsafe extern "C" fn webview_can_go_back(data: *const ()) -> bool {
let handle = unsafe { &*data.cast::<AndroidWebViewHandle>() };
handle.with_env(|env| {
env.call_method(&handle.wrapper, jni_str!("canGoBack"), jni_sig!("()Z"), &[])
.expect("webview_can_go_back: failed to call WebViewWrapper.canGoBack()")
.z()
.expect("webview_can_go_back: canGoBack() did not return boolean")
})
}
unsafe extern "C" fn webview_can_go_forward(data: *const ()) -> bool {
let handle = unsafe { &*data.cast::<AndroidWebViewHandle>() };
handle.with_env(|env| {
env.call_method(
&handle.wrapper,
jni_str!("canGoForward"),
jni_sig!("()Z"),
&[],
)
.expect("webview_can_go_forward: failed to call WebViewWrapper.canGoForward()")
.z()
.expect("webview_can_go_forward: canGoForward() did not return boolean")
})
}
unsafe extern "C" fn webview_set_user_agent(data: *mut (), user_agent: WuiStr) {
let handle = unsafe { &*(data as *const AndroidWebViewHandle) };
let ua = wui_str_to_string(user_agent);
handle.with_env(|env| {
let jua = java_string(env, &ua);
env.call_method(
&handle.wrapper,
jni_str!("setUserAgent"),
jni_sig!("(Ljava/lang/String;)V"),
&[JValue::Object(&jua)],
)
.expect("webview_set_user_agent: failed to call WebViewWrapper.setUserAgent(String)");
});
}
unsafe extern "C" fn webview_set_redirects_enabled(data: *mut (), enabled: *mut WuiComputed<bool>) {
let handle = unsafe { &*(data as *const AndroidWebViewHandle) };
handle.with_env(|env| {
env.call_method(
&handle.wrapper,
jni_str!("setRedirectsEnabled"),
jni_sig!("(J)V"),
&[JValue::Long(enabled as jlong)],
)
.expect(
"webview_set_redirects_enabled: failed to transfer redirect signal to WebViewWrapper",
);
});
}
unsafe extern "C" fn webview_inject_script(
data: *mut (),
key: WuiStr,
script: WuiStr,
time: WuiScriptInjectionTime,
) {
let handle = unsafe { &*(data as *const AndroidWebViewHandle) };
let key = wui_str_to_string(key);
let script = wui_str_to_string(script);
handle.with_env(|env| {
let jkey = java_string(env, &key);
let jscript = java_string(env, &script);
env.call_method(
&handle.wrapper,
jni_str!("injectScript"),
jni_sig!("(Ljava/lang/String;Ljava/lang/String;I)V"),
&[
JValue::Object(&jkey),
JValue::Object(&jscript),
JValue::Int(time as jint),
],
)
.expect(
"webview_inject_script: failed to call WebViewWrapper.injectScript(String, String, int)",
);
});
}
unsafe extern "C" fn webview_watch(data: *mut (), callback: WuiFn<WuiWebViewEvent>) {
let handle = unsafe { &mut *data.cast::<AndroidWebViewHandle>() };
handle.event_callback = Some(Rc::new(callback));
let handle_ptr = handle as *mut AndroidWebViewHandle as jlong;
handle.with_env(|env| {
let cb_class = env
.find_class(jni_str!(
"dev/waterui/android/components/NativeWebViewEventCallback"
))
.expect("NativeWebViewEventCallback class not found");
let cb_obj = env
.new_object(cb_class, jni_sig!("(J)V"), &[JValue::Long(handle_ptr)])
.expect("Failed to create NativeWebViewEventCallback");
env.call_method(
&handle.wrapper,
jni_str!("setEventCallback"),
jni_sig!("(Ldev/waterui/android/components/WebViewEventCallback;)V"),
&[JValue::Object(&cb_obj)],
)
.expect("webview_watch: failed to call WebViewWrapper.setEventCallback(callback)");
});
}
unsafe extern "C" fn webview_add_handler(
data: *mut (),
name: WuiStr,
handler: WuiFn<WuiWebViewMessage>,
) {
let handle = unsafe { &mut *data.cast::<AndroidWebViewHandle>() };
let name = wui_str_to_string(name);
handle.handlers.insert(name.clone(), Rc::new(handler));
let handle_ptr = handle as *mut AndroidWebViewHandle as jlong;
handle.with_env(|env| {
let jname = java_string(env, &name);
env.call_method(
&handle.wrapper,
jni_str!("addHandler"),
jni_sig!("(Ljava/lang/String;J)V"),
&[JValue::Object(&jname), JValue::Long(handle_ptr)],
)
.expect("webview_add_handler: failed to call WebViewWrapper.addHandler(String, long)");
});
}
unsafe extern "C" fn webview_remove_handler(data: *mut (), name: WuiStr) {
let handle = unsafe { &mut *data.cast::<AndroidWebViewHandle>() };
let name = wui_str_to_string(name);
handle.with_env(|env| {
let jname = java_string(env, &name);
env.call_method(
&handle.wrapper,
jni_str!("removeHandler"),
jni_sig!("(Ljava/lang/String;)V"),
&[JValue::Object(&jname)],
)
.expect("webview_remove_handler: failed to call WebViewWrapper.removeHandler(String)");
});
handle.handlers.remove(&name);
}
unsafe extern "C" fn webview_set_bridge_origins(data: *mut (), patterns: WuiStr) {
let handle = unsafe { &*(data as *const AndroidWebViewHandle) };
let patterns = wui_str_to_string(patterns);
handle.with_env(|env| {
let jpatterns = java_string(env, &patterns);
env.call_method(
&handle.wrapper,
jni_str!("setBridgeOrigins"),
jni_sig!("(Ljava/lang/String;)V"),
&[JValue::Object(&jpatterns)],
)
.expect("webview_set_bridge_origins: failed to call WebViewWrapper.setBridgeOrigins");
});
}
unsafe extern "C" fn webview_set_cookie(data: *mut (), cookie: WuiStr) {
let handle = unsafe { &*(data as *const AndroidWebViewHandle) };
let cookie = wui_str_to_string(cookie);
handle.with_env(|env| {
let jcookie = java_string(env, &cookie);
env.call_method(
&handle.wrapper,
jni_str!("setCookie"),
jni_sig!("(Ljava/lang/String;)V"),
&[JValue::Object(&jcookie)],
)
.expect("webview_set_cookie: failed to call WebViewWrapper.setCookie(String)");
});
}
unsafe extern "C" fn webview_get_cookies(data: *const (), callback: WuiStringCallback) {
let handle = unsafe { &*data.cast::<AndroidWebViewHandle>() };
let call_ptr = callback.call as usize as jlong;
handle.with_env(|env| {
env.call_method(
&handle.wrapper,
jni_str!("getCookies"),
jni_sig!("(JJ)V"),
&[JValue::Long(callback.data as jlong), JValue::Long(call_ptr)],
)
.expect("webview_get_cookies: failed to call getCookies(callback)");
});
}
unsafe extern "C" fn webview_run_javascript(
data: *mut (),
script: WuiStr,
callback: WuiJsCallback,
) {
let handle = unsafe { &*(data as *const AndroidWebViewHandle) };
let script = wui_str_to_string(script);
let call_ptr = callback.call as usize as jlong;
handle.with_env(|env| {
let jscript = java_string(env, &script);
env.call_method(
&handle.wrapper,
jni_str!("runJavaScript"),
jni_sig!("(Ljava/lang/String;JJ)V"),
&[
JValue::Object(&jscript),
JValue::Long(callback.data as jlong),
JValue::Long(call_ptr),
],
)
.expect("webview_run_javascript: failed to call WebViewWrapper.runJavaScript");
});
}
unsafe extern "C" fn webview_call_async_javascript(
data: *mut (),
body: WuiStr,
callback: WuiJsCallback,
) {
let handle = unsafe { &*(data as *const AndroidWebViewHandle) };
let body = wui_str_to_string(body);
let call_ptr = callback.call as usize as jlong;
handle.with_env(|env| {
let jbody = java_string(env, &body);
env.call_method(
&handle.wrapper,
jni_str!("callAsyncJavaScript"),
jni_sig!("(Ljava/lang/String;JJ)V"),
&[
JValue::Object(&jbody),
JValue::Long(callback.data as jlong),
JValue::Long(call_ptr),
],
)
.expect("webview_call_async_javascript: failed to call WebViewWrapper.callAsyncJavaScript");
});
}
unsafe extern "C" fn webview_drop(data: *mut ()) {
let mut handle = unsafe { Box::from_raw(data as *mut AndroidWebViewHandle) };
handle.with_env(|env| {
env.call_method(
&handle.wrapper,
jni_str!("setEventCallback"),
jni_sig!("(Ldev/waterui/android/components/WebViewEventCallback;)V"),
&[JValue::Object(&JObject::null())],
)
.expect("webview_drop: failed to clear WebViewWrapper event callback");
env.call_method(&handle.wrapper, jni_str!("release"), jni_sig!("()V"), &[])
.expect("webview_drop: failed to call WebViewWrapper.release()");
});
handle.event_callback.take();
handle.handlers.clear();
}
pub unsafe fn install_android_webview_controller(
env: &Env,
wui_env: *mut crate::WuiEnv,
factory: JObject,
) {
let controller = WebViewController::new(AndroidWebViewFactory {
jvm: Arc::new(
env.get_java_vm()
.expect("WebView factory installation failed to access JavaVM"),
),
factory: env
.new_global_ref(factory)
.expect("WebView factory installation failed to retain Java factory"),
});
let env = unsafe { crate::borrow_ffi_mut(wui_env) };
env.0.insert(controller);
}
pub(crate) fn webview_native_view<'local>(env: &mut Env<'local>, handle_ptr: jlong) -> jobject {
let handle = unsafe { &*(handle_ptr as *mut AndroidWebViewHandle) };
env.call_method(
&handle.wrapper,
jni_str!("getWebView"),
jni_sig!("()Landroid/webkit/WebView;"),
&[],
)
.expect("webview_native_view: failed to call WebViewWrapper.getWebView()")
.l()
.expect("webview_native_view: getWebView() did not return an object")
.into_raw()
}
type JsCallbackFn = unsafe extern "C" fn(*mut (), bool, WuiStr);
type StringCallbackFn = unsafe extern "C" fn(*mut (), WuiStr);
#[unsafe(no_mangle)]
extern "system" fn Java_dev_waterui_android_components_WebViewWrapper_nativeCompleteCookies<
'local,
>(
mut env: EnvUnowned<'local>,
_this: JObject<'local>,
callback_data: jlong,
callback_fn: jlong,
result: JString<'local>,
) {
let call: StringCallbackFn =
unsafe { core::mem::transmute::<usize, StringCallbackFn>(callback_fn as usize) };
super::with_env(&mut env, |env| {
let text = result
.try_to_string(env)
.expect("WebViewWrapper.nativeCompleteCookies: result");
unsafe {
call(
callback_data as *mut (),
waterui::Str::from(text).into_ffi(),
);
}
});
}
#[unsafe(no_mangle)]
extern "system" fn Java_dev_waterui_android_components_WebViewWrapper_nativeCompleteJsResult<
'local,
>(
mut env: EnvUnowned<'local>,
_this: JObject<'local>,
callback_data: jlong,
callback_fn: jlong,
success: jboolean,
result: JString<'local>,
) {
let call: JsCallbackFn =
unsafe { core::mem::transmute::<usize, JsCallbackFn>(callback_fn as usize) };
super::with_env(&mut env, |env| {
let text = result
.try_to_string(env)
.expect("WebViewWrapper.nativeCompleteJsResult: result");
let wui_str = waterui::Str::from(text).into_ffi();
unsafe {
call(callback_data as *mut (), success, wui_str);
}
});
}
struct ReplyCtx {
jvm: Arc<JavaVM>,
wrapper: Global<JObject<'static>>,
request_id: u64,
}
unsafe extern "C" fn reply_call(
data: *mut (),
success: bool,
kind: WuiJsReplyKind,
payload_b64: WuiStr,
) {
let ctx = unsafe { Box::from_raw(data as *mut ReplyCtx) };
let payload: waterui::Str = unsafe { payload_b64.into_rust() };
let reply = if success {
match base64::engine::general_purpose::STANDARD.decode(payload.as_str()) {
Ok(bytes) => match kind {
WuiJsReplyKind::Json => bridge::Reply::Json(bytes),
WuiJsReplyKind::Bytes => bridge::Reply::Bytes(bytes),
},
Err(error) => bridge::Reply::failure(&error),
}
} else {
bridge::Reply::failure(&payload.as_str())
};
let script = reply.resolve_script(ctx.request_id);
super::with_attached_env(&ctx.jvm, |env| {
let jscript = java_string(env, &script);
env.call_method(
&ctx.wrapper,
jni_str!("evaluateBridgeScript"),
jni_sig!("(Ljava/lang/String;)V"),
&[JValue::Object(&jscript)],
)
.expect("reply_call: failed to call WebViewWrapper.evaluateBridgeScript(String)");
})
.expect("WebView reply failed to attach its JVM thread");
}
#[unsafe(no_mangle)]
extern "system" fn Java_dev_waterui_android_components_WebViewWrapper_nativeBridgeScript<'local>(
mut env: EnvUnowned<'local>,
_this: JObject<'local>,
) -> jobject {
super::with_env(&mut env, |env| {
java_string(env, waterui_webview::DOCUMENT_START_SCRIPT).into_raw()
})
}
#[unsafe(no_mangle)]
extern "system" fn Java_dev_waterui_android_components_WebViewWrapper_nativeOnBridgeMessage<
'local,
>(
mut env: EnvUnowned<'local>,
_this: JObject<'local>,
native_ptr: jlong,
envelope: JString<'local>,
) {
super::with_env(&mut env, |env| {
let Ok(envelope) = envelope.try_to_string(env) else {
tracing::warn!("WaterUI bridge received a non-UTF-8 envelope; ignoring");
return;
};
let handle = unsafe { &*(native_ptr as *const AndroidWebViewHandle) };
let request = match bridge::Request::parse(&envelope) {
Ok(request) => request,
Err(error) => {
tracing::warn!(%error, "page script sent a malformed WaterUI bridge request");
return;
}
};
let mut reply_to_page = |reply: &bridge::Reply| {
let script = reply.resolve_script(request.id);
let jscript = java_string(env, &script);
env.call_method(
&handle.wrapper,
jni_str!("evaluateBridgeScript"),
jni_sig!("(Ljava/lang/String;)V"),
&[JValue::Object(&jscript)],
)
.expect("failed to call WebViewWrapper.evaluateBridgeScript(String)");
};
let Some(handler) = handle.handlers.get(&request.name).map(Rc::clone) else {
tracing::warn!(
handler = %request.name,
"page script called a WaterUI handler that is not registered"
);
reply_to_page(&bridge::Reply::failure(&format!(
"no WaterUI handler named `{}`",
request.name
)));
return;
};
let reply_ctx = Box::new(ReplyCtx {
jvm: Arc::clone(&handle.jvm),
wrapper: env
.new_global_ref(handle.wrapper.as_obj())
.expect("failed to clone the WebViewWrapper reference"),
request_id: request.id,
});
let msg = WuiWebViewMessage {
payload_base64: waterui::Str::from(
base64::engine::general_purpose::STANDARD.encode(&request.payload),
)
.into_ffi(),
reply: WuiWebViewReply {
data: Box::into_raw(reply_ctx) as *mut (),
call: reply_call,
},
};
handler.call(msg);
});
}
#[unsafe(no_mangle)]
extern "system" fn Java_dev_waterui_android_components_NativeWebViewEventCallback_nativeOnEvent<
'local,
>(
mut env: EnvUnowned<'local>,
_this: JObject<'local>,
native_ptr: jlong,
event_type: jint,
url: JString<'local>,
url2: JString<'local>,
message: JString<'local>,
progress: jfloat,
can_go_back: jboolean,
can_go_forward: jboolean,
) {
let callback = unsafe { &*(native_ptr as *const AndroidWebViewHandle) }
.event_callback
.as_ref()
.cloned()
.expect("webview.native_on_event missing registered Rust callback");
let event_type = match event_type {
1 => WuiWebViewEventType::WillNavigate,
2 => WuiWebViewEventType::Loading,
3 => WuiWebViewEventType::Loaded,
4 => WuiWebViewEventType::Redirect,
5 => WuiWebViewEventType::SslError,
6 => WuiWebViewEventType::Error,
7 => WuiWebViewEventType::StateChanged,
_ => panic!("webview.native_on_event received unknown event type {event_type}"),
};
fn take_java_string(env: &mut Env, value: &JString, field: &'static str) -> *mut WuiStr {
let value = value
.try_to_string(env)
.unwrap_or_else(|_| panic!("webview.native_on_event requires {field}"));
Box::into_raw(Box::new(waterui::Str::from(value).into_ffi()))
}
super::with_env(&mut env, |env| {
let (url, url2, message) = match event_type {
WuiWebViewEventType::WillNavigate => (
take_java_string(env, &url, "url"),
core::ptr::null_mut(),
core::ptr::null_mut(),
),
WuiWebViewEventType::Redirect => (
take_java_string(env, &url, "url"),
take_java_string(env, &url2, "url2"),
core::ptr::null_mut(),
),
WuiWebViewEventType::SslError => (
take_java_string(env, &url, "url"),
core::ptr::null_mut(),
take_java_string(env, &message, "message"),
),
WuiWebViewEventType::Error => (
core::ptr::null_mut(),
core::ptr::null_mut(),
take_java_string(env, &message, "message"),
),
WuiWebViewEventType::Loading
| WuiWebViewEventType::Loaded
| WuiWebViewEventType::StateChanged => (
core::ptr::null_mut(),
core::ptr::null_mut(),
core::ptr::null_mut(),
),
};
let event = WuiWebViewEvent {
event_type,
url,
url2,
message,
progress,
can_go_back,
can_go_forward,
};
callback.call(event);
});
}