use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use crate::input_helper::{build_wrapped_eval_script, parse_wrapped_eval_result};
use crate::webview::{
EffectiveWebViewCreateOptions, ProxyActivation, ProxyApplyReport, ProxyConfig, WebTag,
WebViewCreateSender, WebViewCreateStage,
};
use crate::{LoadDataRequest, WebViewController, WebViewError, WebViewScriptError};
use async_trait::async_trait;
use jni::objects::{Global, JObject};
use jni::{jni_sig, jni_str};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
use tokio::sync::oneshot;
use tokio::time::timeout;
use super::jni_env::{get_lingxia_webview_class, with_env};
fn encode_options_token(options: &EffectiveWebViewCreateOptions) -> Result<String, WebViewError> {
let json = serde_json::to_vec(options).map_err(|e| {
WebViewError::InvalidCreateOptions(format!("Serialize options failed: {e}"))
})?;
Ok(URL_SAFE_NO_PAD.encode(json))
}
pub(crate) struct PendingWebViewCreation {
pub sender: WebViewCreateSender,
pub effective_options: EffectiveWebViewCreateOptions,
}
type WebViewSendersMap = Arc<Mutex<HashMap<String, PendingWebViewCreation>>>;
type PendingEvalRequests = Arc<Mutex<HashMap<u64, PendingEvalEntry>>>;
enum PendingEvalResponse {
Success(String),
Failure(String),
Destroyed,
}
struct PendingEvalEntry {
webtag: String,
sender: oneshot::Sender<PendingEvalResponse>,
}
pub(crate) static WEBVIEW_SENDERS: OnceLock<WebViewSendersMap> = OnceLock::new();
static PENDING_EVAL_REQUESTS: OnceLock<PendingEvalRequests> = OnceLock::new();
static NEXT_EVAL_REQUEST_ID: AtomicU64 = AtomicU64::new(1);
const EVAL_TIMEOUT: Duration = Duration::from_secs(10);
fn pending_eval_requests() -> &'static PendingEvalRequests {
PENDING_EVAL_REQUESTS.get_or_init(|| Arc::new(Mutex::new(HashMap::new())))
}
pub(crate) fn complete_pending_eval_request(request_id: u64, result: Result<String, String>) {
if let Ok(mut pending) = pending_eval_requests().lock()
&& let Some(entry) = pending.remove(&request_id)
{
let message = match result {
Ok(value) => PendingEvalResponse::Success(value),
Err(error) => PendingEvalResponse::Failure(error),
};
let _ = entry.sender.send(message);
}
}
fn fail_pending_eval_requests_for_webtag(webtag: &WebTag) {
if let Ok(mut pending) = pending_eval_requests().lock() {
let matching = pending
.iter()
.filter_map(|(request_id, entry)| {
(entry.webtag == webtag.as_str()).then_some(*request_id)
})
.collect::<Vec<_>>();
for request_id in matching {
if let Some(entry) = pending.remove(&request_id) {
let _ = entry.sender.send(PendingEvalResponse::Destroyed);
}
}
}
}
fn decode_android_eval_string(raw: &str) -> Result<String, WebViewScriptError> {
let decoded: Option<String> = serde_json::from_str(raw).map_err(|err| {
WebViewScriptError::Platform(format!(
"Failed to decode Android evaluateJavascript payload: {err}"
))
})?;
decoded.ok_or_else(|| {
WebViewScriptError::Platform("Android evaluateJavascript returned null result".to_string())
})
}
pub(crate) fn apply_http_proxy(
config: Option<&ProxyConfig>,
) -> Result<ProxyApplyReport, WebViewError> {
let host = config.map(|cfg| cfg.host.as_str());
let port = config.map(|cfg| cfg.port as i32).unwrap_or(0);
let bypass = config.map(|cfg| cfg.bypass.clone()).unwrap_or_default();
with_env(
|env| -> Result<ProxyApplyReport, Box<dyn std::error::Error>> {
let webview_class =
get_lingxia_webview_class().ok_or("LingXiaWebView class not cached")?;
let host_obj = match host {
Some(value) => JObject::from(env.new_string(value)?),
None => JObject::null(),
};
let bypass_array = env.new_object_array(
bypass.len() as i32,
jni_str!("java/lang/String"),
JObject::null(),
)?;
for (idx, rule) in bypass.iter().enumerate() {
let rule_string = env.new_string(rule)?;
bypass_array.set_element(env, idx, &rule_string)?;
}
let bypass_arg = JObject::from(bypass_array);
let result = env.call_static_method(
webview_class,
jni_str!("applyHttpProxy"),
jni_sig!("(Ljava/lang/String;I[Ljava/lang/String;)Ljava/lang/String;"),
&[(&host_obj).into(), port.into(), (&bypass_arg).into()],
)?;
let error_obj = result.l()?;
if !error_obj.is_null() {
let error_jstring = jni::objects::JString::cast_local(env, error_obj)?;
let error = error_jstring.try_to_string(env)?;
if let Some(detail) = error.strip_prefix("UNSUPPORTED:") {
return Ok(ProxyApplyReport::unsupported(detail.trim()));
}
return Err(format!("Android proxy apply failed: {}", error).into());
}
let report = if config.is_some() {
ProxyApplyReport::applied(ProxyActivation::EffectiveNow)
} else {
ProxyApplyReport::cleared(ProxyActivation::EffectiveNow)
};
Ok(report)
},
)
.map_err(|e| WebViewError::WebView(format!("Failed to apply Android proxy: {:?}", e)))
}
#[derive(Debug)]
pub struct WebViewInner {
java_webview: Option<Global<JObject<'static>>>,
pub(crate) webtag: WebTag,
}
impl WebViewInner {
pub(crate) fn create(
appid: &str,
path: &str,
session_id: Option<u64>,
effective_options: EffectiveWebViewCreateOptions,
sender: WebViewCreateSender,
) {
let webtag = WebTag::new(appid, path, session_id);
let senders = WEBVIEW_SENDERS.get_or_init(|| Arc::new(Mutex::new(HashMap::new())));
if let Ok(mut senders_map) = senders.lock() {
senders_map.insert(
webtag.to_string(),
PendingWebViewCreation {
sender,
effective_options: effective_options.clone(),
},
);
}
let remove_and_send_error = |error_msg: String| {
if let Ok(mut senders_map) = senders.lock()
&& let Some(pending) = senders_map.remove(&webtag.to_string())
{
pending.sender.fail(
WebViewCreateStage::Requested,
WebViewError::WebView(error_msg),
);
}
};
let appid_owned = appid.to_string();
let path_owned = path.to_string();
let options_token = match encode_options_token(&effective_options) {
Ok(token) => token,
Err(e) => {
remove_and_send_error(format!("Failed to encode create options token: {e}"));
return;
}
};
let result = with_env(|env| -> Result<(), Box<dyn std::error::Error>> {
let webview_class =
get_lingxia_webview_class().ok_or("LingXiaWebView class not cached")?;
let appid_jstring = env.new_string(&appid_owned)?;
let path_jstring = env.new_string(&path_owned)?;
let session = session_id.unwrap_or_default() as i64;
let options_jstring = env.new_string(&options_token)?;
env.call_static_method(
webview_class,
jni_str!("requestWebView"),
jni_sig!("(Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;)V"),
&[
(&appid_jstring).into(),
(&path_jstring).into(),
session.into(),
(&options_jstring).into(),
],
)?;
log::info!(
"Successfully requested WebView creation for {}-{}",
appid_owned,
path_owned
);
Ok(())
});
if let Err(e) = result {
log::error!("Failed to request WebView creation: {:?}", e);
remove_and_send_error(format!("Failed to request WebView creation: {:?}", e));
}
}
pub(crate) fn from_java_object(java_webview: Global<JObject<'static>>, webtag: WebTag) -> Self {
WebViewInner {
java_webview: Some(java_webview),
webtag,
}
}
pub fn get_java_webview(&self) -> &Global<JObject<'static>> {
self.java_webview
.as_ref()
.expect("Android WebView global reference is missing")
}
}
impl Drop for WebViewInner {
fn drop(&mut self) {
fail_pending_eval_requests_for_webtag(&self.webtag);
let Some(java_webview) = self.java_webview.take() else {
return;
};
let _ = with_env(move |env| -> Result<(), Box<dyn std::error::Error>> {
let _ = env.call_method(&*java_webview, jni_str!("destroy"), jni_sig!("()V"), &[]);
drop(java_webview);
Ok(())
});
log::info!(
"[WebViewInner] Android WebViewInner dropped and destroyed ({})",
self.webtag.as_str()
);
}
}
#[async_trait]
impl WebViewController for WebViewInner {
fn load_url(&self, url: &str) -> Result<(), WebViewError> {
with_env(|env| -> Result<(), Box<dyn std::error::Error>> {
let url_string = env.new_string(&url)?;
env.call_method(
&*self.get_java_webview(),
jni_str!("loadUrl"),
jni_sig!("(Ljava/lang/String;)V"),
&[(&url_string).into()],
)?;
Ok(())
})
.map_err(|e| WebViewError::WebView(format!("Failed to load URL: {:?}", e)))
}
fn load_data(&self, request: LoadDataRequest<'_>) -> Result<(), WebViewError> {
with_env(|env| -> Result<(), Box<dyn std::error::Error>> {
let data_string = env.new_string(request.data)?;
let base_url_string = env.new_string(request.base_url)?;
let history_url_string = match request.history_url {
Some(url) => env.new_string(&url)?,
None => env.new_string(request.base_url)?,
};
env.call_method(
&*self.get_java_webview(),
jni_str!("loadHtmlData"),
jni_sig!("(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"),
&[
(&data_string).into(),
(&base_url_string).into(),
(&history_url_string).into(),
],
)?;
Ok(())
})
.map_err(|e| WebViewError::WebView(format!("Failed to load data: {:?}", e)))
}
fn exec_js(&self, js: &str) -> Result<(), WebViewError> {
with_env(|env| -> Result<(), Box<dyn std::error::Error>> {
let script_string = env.new_string(js)?;
env.call_method(
&*self.get_java_webview(),
jni_str!("evaluateJavascript"),
jni_sig!("(Ljava/lang/String;Landroid/webkit/ValueCallback;)V"),
&[(&script_string).into(), (&JObject::null()).into()],
)?;
Ok(())
})
.map_err(|e| WebViewError::WebView(format!("JavaScript execution failed: {:?}", e)))
}
async fn eval_js(&self, js: &str) -> Result<serde_json::Value, WebViewScriptError> {
let wrapped = build_wrapped_eval_script(js)?;
let request_id = NEXT_EVAL_REQUEST_ID.fetch_add(1, Ordering::Relaxed);
let (tx, rx) = oneshot::channel();
pending_eval_requests()
.lock()
.map_err(|_| {
WebViewScriptError::Platform("Android pending eval_js map poisoned".to_string())
})?
.insert(
request_id,
PendingEvalEntry {
webtag: self.webtag.to_string(),
sender: tx,
},
);
{
let dispatch_result = with_env(|env| -> Result<(), Box<dyn std::error::Error>> {
let script_string = env.new_string(&wrapped)?;
env.call_method(
&*self.get_java_webview(),
jni_str!("evaluateJavascriptWithResult"),
jni_sig!("(Ljava/lang/String;J)V"),
&[(&script_string).into(), (request_id as i64).into()],
)?;
Ok(())
});
if let Err(err) = dispatch_result {
if let Ok(mut pending) = pending_eval_requests().lock() {
pending.remove(&request_id);
}
return Err(WebViewScriptError::Platform(format!(
"Failed to dispatch Android JavaScript evaluation: {:?}",
err
)));
}
}
let raw = match timeout(EVAL_TIMEOUT, rx).await {
Ok(Ok(PendingEvalResponse::Success(raw))) => raw,
Ok(Ok(PendingEvalResponse::Failure(err))) => {
return Err(WebViewScriptError::Platform(err));
}
Ok(Ok(PendingEvalResponse::Destroyed)) => return Err(WebViewScriptError::Destroyed),
Ok(Err(_)) => return Err(WebViewScriptError::Destroyed),
Err(_) => {
if let Ok(mut pending) = pending_eval_requests().lock() {
pending.remove(&request_id);
}
return Err(WebViewScriptError::Timeout);
}
};
let decoded = decode_android_eval_string(&raw)?;
parse_wrapped_eval_result(&decoded)
}
fn clear_browsing_data(&self) -> Result<(), WebViewError> {
with_env(|env| -> Result<(), Box<dyn std::error::Error>> {
env.call_method(
&*self.get_java_webview(),
jni_str!("clearBrowsingData"),
jni_sig!("()V"),
&[],
)?;
Ok(())
})
.map_err(|e| WebViewError::WebView(format!("Failed to clear browsing data: {:?}", e)))
}
fn post_message(&self, message: &str) -> Result<(), WebViewError> {
with_env(|env| -> Result<(), Box<dyn std::error::Error>> {
let msg_string = env.new_string(&message)?;
env.call_method(
&*self.get_java_webview(),
jni_str!("postMessageToWebView"),
jni_sig!("(Ljava/lang/String;)V"),
&[(&msg_string).into()],
)?;
Ok(())
})
.map_err(|e| WebViewError::WebView(format!("Failed to post message: {:?}", e)))
}
fn set_user_agent(&self, ua: &str) -> Result<(), WebViewError> {
with_env(|env| -> Result<(), Box<dyn std::error::Error>> {
let ua_string = env.new_string(&ua)?;
env.call_method(
&*self.get_java_webview(),
jni_str!("setUserAgent"),
jni_sig!("(Ljava/lang/String;)V"),
&[(&ua_string).into()],
)?;
Ok(())
})
.map_err(|e| WebViewError::WebView(format!("Failed to set user agent: {:?}", e)))
}
}