use crate::traits::{
FileChooserRequest, FileChooserResponse, LoadError, LoadErrorKind, NavigationPolicy,
};
use crate::webview::{
WebTag, WebViewCreateStage, find_webview, find_webview_delegate, register_webview,
};
use crate::{DownloadRequest, LogLevel, WebResourceBody, WebResourceResponse, WebViewError};
use http::header::{HeaderMap, HeaderName, HeaderValue};
use http::{Method, Request};
use jni::objects::{JByteArray, JObject, JObjectArray, JString, JValue};
use jni::sys::{jboolean, jint, jlong};
use jni::{Env, EnvUnowned, errors::ThrowRuntimeExAndDefault, jni_sig, jni_str};
use std::fs;
use std::sync::Arc;
use crate::android::webview::{WEBVIEW_SENDERS, WebViewInner, complete_pending_eval_request};
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_lingxia_webview_LingXiaWebView_handlePostMessage(
mut env: EnvUnowned,
_this: JObject,
appid: JString,
path: JString,
session_id: jlong,
message: JString,
) -> jint {
env.with_env(|env| -> Result<jint, jni::errors::Error> {
let appid: String = appid.try_to_string(env)?;
let path: String = path.try_to_string(env)?;
let message: String = message.try_to_string(env)?;
let session_id = if session_id > 0 {
Some(session_id as u64)
} else {
None
};
let webtag = WebTag::new(&appid, &path, session_id);
if let Some(delegate) = find_webview_delegate(&webtag) {
delegate.handle_post_message(message);
}
Ok(0)
})
.resolve::<ThrowRuntimeExAndDefault>()
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_lingxia_webview_LingXiaWebView_onPageStarted(
mut env: EnvUnowned,
_this: JObject,
appid: JString,
path: JString,
session_id: jlong,
) -> jint {
env.with_env(|env| -> Result<jint, jni::errors::Error> {
let appid: String = appid.try_to_string(env)?;
let path: String = path.try_to_string(env)?;
let session_id = if session_id > 0 {
Some(session_id as u64)
} else {
None
};
let webtag = WebTag::new(&appid, &path, session_id);
if let Some(delegate) = find_webview_delegate(&webtag) {
delegate.on_page_started();
}
Ok(0)
})
.resolve::<ThrowRuntimeExAndDefault>()
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_lingxia_webview_LingXiaWebView_onPageFinished(
mut env: EnvUnowned,
_this: JObject,
appid: JString,
path: JString,
session_id: jlong,
) -> jint {
env.with_env(|env| -> Result<jint, jni::errors::Error> {
let appid: String = appid.try_to_string(env)?;
let path: String = path.try_to_string(env)?;
let session_id = if session_id > 0 {
Some(session_id as u64)
} else {
None
};
let webtag = WebTag::new(&appid, &path, session_id);
if let Some(delegate) = find_webview_delegate(&webtag) {
delegate.on_page_finished();
}
Ok(0)
})
.resolve::<ThrowRuntimeExAndDefault>()
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_lingxia_webview_LingXiaWebView_onEvaluateJavascriptResult(
mut env: EnvUnowned,
_this: JObject,
request_id: jlong,
value: JString,
error: JString,
) {
env.with_env(|env| -> Result<(), jni::errors::Error> {
let request_id = request_id as u64;
let value: String = value.try_to_string(env)?;
let error: String = error.try_to_string(env)?;
if error.trim().is_empty() {
complete_pending_eval_request(request_id, Ok(value));
} else {
complete_pending_eval_request(request_id, Err(error));
}
Ok(())
})
.resolve::<ThrowRuntimeExAndDefault>()
}
fn android_load_error_kind(error_code: i32, description: &str) -> LoadErrorKind {
match error_code {
-2 => LoadErrorKind::Dns,
-6 | -7 | -3 | -4 | -5 | -9 | -15 => LoadErrorKind::Network,
-8 => LoadErrorKind::Timeout,
-11 | -16 => LoadErrorKind::Security,
-10 | -12 => LoadErrorKind::InvalidUrl,
-14 => LoadErrorKind::NotFound,
_ => {
let desc = description.trim().to_ascii_lowercase();
if desc.is_empty() {
LoadErrorKind::Unknown
} else if desc.contains("dns")
|| desc.contains("host")
|| desc.contains("name not resolved")
{
LoadErrorKind::Dns
} else if desc.contains("timeout") || desc.contains("timed out") {
LoadErrorKind::Timeout
} else if desc.contains("ssl")
|| desc.contains("tls")
|| desc.contains("certificate")
|| desc.contains("secure connection")
{
LoadErrorKind::Security
} else if desc.contains("cancel") || desc.contains("aborted") {
LoadErrorKind::Cancelled
} else if desc.contains("bad url")
|| desc.contains("invalid url")
|| desc.contains("malformed")
|| desc.contains("unsupported scheme")
{
LoadErrorKind::InvalidUrl
} else if desc.contains("not found") || desc.contains("no such file") {
LoadErrorKind::NotFound
} else if desc.contains("network")
|| desc.contains("offline")
|| desc.contains("internet")
|| desc.contains("connect")
|| desc.contains("connection")
{
LoadErrorKind::Network
} else {
LoadErrorKind::Unknown
}
}
}
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_lingxia_webview_LingXiaWebView_onLoadError(
mut env: EnvUnowned,
_this: JObject,
appid: JString,
path: JString,
session_id: jlong,
url: JString,
error_code: jint,
description: JString,
) {
env.with_env(|env| -> Result<(), jni::errors::Error> {
let appid: String = appid.try_to_string(env)?;
let path: String = path.try_to_string(env)?;
let session_id = if session_id > 0 {
Some(session_id as u64)
} else {
None
};
let url: String = url.try_to_string(env)?;
let description: String = description.try_to_string(env)?;
let webtag = WebTag::new(&appid, &path, session_id);
if let Some(delegate) = find_webview_delegate(&webtag) {
delegate.on_load_error(&LoadError {
url: if url.is_empty() { None } else { Some(url) },
kind: android_load_error_kind(error_code, &description),
description,
});
}
Ok(())
})
.resolve::<ThrowRuntimeExAndDefault>()
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_lingxia_webview_LingXiaWebView_onDownloadRequested(
mut env: EnvUnowned,
_this: JObject,
appid: JString,
path: JString,
session_id: jlong,
url: JString,
user_agent: JString,
content_disposition: JString,
mime_type: JString,
content_length: jlong,
cookie: JString,
) {
env.with_env(|env| -> Result<(), jni::errors::Error> {
let appid: String = appid.try_to_string(env)?;
let path: String = path.try_to_string(env)?;
let url: String = url.try_to_string(env)?;
let user_agent: String = user_agent.try_to_string(env)?;
let content_disposition: String = content_disposition.try_to_string(env)?;
let mime_type: String = mime_type.try_to_string(env)?;
let cookie: String = cookie.try_to_string(env)?;
let session_id = if session_id > 0 {
Some(session_id as u64)
} else {
None
};
let webtag = WebTag::new(&appid, &path, session_id);
let request = DownloadRequest {
url,
user_agent: if user_agent.trim().is_empty() {
None
} else {
Some(user_agent)
},
content_disposition: if content_disposition.trim().is_empty() {
None
} else {
Some(content_disposition)
},
mime_type: if mime_type.trim().is_empty() {
None
} else {
Some(mime_type)
},
content_length: if content_length >= 0 {
Some(content_length as u64)
} else {
None
},
suggested_filename: None,
source_page_url: None,
cookie: if cookie.trim().is_empty() {
None
} else {
Some(cookie)
},
};
if let Some(webview) = find_webview(&webtag) {
webview.handle_download(request);
}
Ok(())
})
.resolve::<ThrowRuntimeExAndDefault>()
}
fn complete_android_file_chooser(webtag: WebTag, request_id: u64, response: FileChooserResponse) {
let Some(webview) = find_webview(&webtag) else {
return;
};
let java_webview = webview.get_java_webview();
let _ = crate::android::with_env(move |env| {
let selected_paths = match response {
FileChooserResponse::Cancel => JObject::null(),
FileChooserResponse::Error(message) => {
log::warn!("Android file chooser failed: {}", message);
JObject::null()
}
FileChooserResponse::Files(files) => {
let string_class = env.find_class(jni_str!("java/lang/String"))?;
let array =
env.new_object_array(files.len() as i32, string_class, JObject::null())?;
for (index, file) in files.into_iter().enumerate() {
let raw = match (file.uri, file.path) {
(Some(uri), _) => uri,
(None, Some(path)) => {
if path.contains("://") {
path
} else {
format!("file://{path}")
}
}
(None, None) => String::new(),
};
let java_path = env.new_string(raw)?;
array.set_element(env, index, java_path)?;
}
JObject::from(array)
}
};
let request_id = request_id as i64;
let _ = env.call_method(
java_webview.as_obj(),
jni_str!("completeFileChooserRequest"),
jni_sig!("(J[Ljava/lang/String;)V"),
&[JValue::Long(request_id), JValue::Object(&selected_paths)],
)?;
Ok::<(), jni::errors::Error>(())
});
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_lingxia_webview_LingXiaWebView_onFileChooserRequested(
mut env: EnvUnowned,
_this: JObject,
appid: JString,
path: JString,
session_id: jlong,
request_id: jlong,
source_url: JString,
accept_types: JObjectArray,
allow_multiple: jboolean,
allow_directories: jboolean,
capture: jboolean,
) {
env.with_env(|env| -> Result<(), jni::errors::Error> {
let appid: String = appid.try_to_string(env)?;
let path: String = path.try_to_string(env)?;
let source_url: String = source_url.try_to_string(env)?;
let session_id = if session_id > 0 {
Some(session_id as u64)
} else {
None
};
let webtag = WebTag::new(&appid, &path, session_id);
let mut accepted = Vec::new();
let len = accept_types.len(env)?;
for index in 0..len {
let value = accept_types.get_element(env, index)?;
if value.is_null() {
continue;
}
let value = unsafe { JString::from_raw(env, value.into_raw()) };
let parsed: String = value.try_to_string(env)?;
if !parsed.trim().is_empty() {
accepted.push(parsed);
}
}
let request = FileChooserRequest {
accept_types: accepted,
allow_multiple: allow_multiple,
allow_directories: allow_directories,
capture,
source_page_url: (!source_url.trim().is_empty()).then_some(source_url),
};
if let Some(webview) = find_webview(&webtag) {
let webtag_for_callback = webtag.clone();
let handled = webview.handle_file_chooser(request, move |response| {
complete_android_file_chooser(
webtag_for_callback.clone(),
request_id as u64,
response,
);
});
if !handled {
complete_android_file_chooser(
webtag,
request_id as u64,
FileChooserResponse::Cancel,
);
}
} else {
let request_id = request_id as i64;
let selected_paths = JObject::null();
let _ = env.call_method(
&_this,
jni_str!("completeFileChooserRequest"),
jni_sig!("(J[Ljava/lang/String;)V"),
&[JValue::Long(request_id), JValue::Object(&selected_paths)],
)?;
}
Ok(())
})
.resolve::<ThrowRuntimeExAndDefault>()
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_lingxia_webview_LingXiaWebView_handleRequest<'a>(
mut env: EnvUnowned<'a>,
_this: JObject<'a>,
appid: JString<'a>,
path: JString<'a>,
session_id: jlong,
url: JString<'a>,
method: JString<'a>,
headers_array: jni::sys::jobjectArray,
) -> JObject<'a> {
env.with_env(|env| -> Result<JObject<'a>, jni::errors::Error> {
let appid: String = appid.try_to_string(env)?;
let path: String = path.try_to_string(env)?;
let url_str: String = url.try_to_string(env)?;
let method_str: String = method.try_to_string(env)?;
let session_id = if session_id > 0 {
Some(session_id as u64)
} else {
None
};
let mut http_headers = HeaderMap::new();
if !headers_array.is_null() {
let headers_array = unsafe { JObjectArray::<JString>::from_raw(env, headers_array) };
match headers_array.len(env) {
Ok(array_len) => {
for i in (0..array_len).step_by(2) {
if i + 1 < array_len {
if let (Ok(key_obj), Ok(value_obj)) = (
headers_array.get_element(env, i as usize),
headers_array.get_element(env, (i + 1) as usize),
) {
let key_jstring = key_obj;
let value_jstring = value_obj;
if let (Ok(key_str), Ok(value_str)) = (
key_jstring.try_to_string(env),
value_jstring.try_to_string(env),
) {
if let (Ok(name), Ok(val)) = (
HeaderName::from_bytes(key_str.as_bytes()),
HeaderValue::from_str(&value_str),
) {
http_headers.insert(name, val);
}
}
}
}
}
}
Err(_) => {
}
}
}
let http_method = method_str.parse::<Method>().unwrap_or(Method::GET);
let request = match Request::builder()
.method(http_method)
.uri(url_str)
.body(Vec::new())
{
Ok(mut req) => {
*req.headers_mut() = http_headers;
req
}
Err(_) => return Ok(JObject::null()),
};
let webtag = WebTag::new(&appid, &path, session_id);
let scheme = request.uri().scheme_str().unwrap_or("").to_string();
let response = if let Some(webview) = find_webview(&webtag) {
webview.handle_scheme_request(&scheme, request)
} else {
None
};
if let Some(response) = response {
Ok(create_java_response(env, response)?)
} else {
Ok(JObject::null())
}
})
.resolve::<ThrowRuntimeExAndDefault>()
}
fn create_java_response<'a>(
env: &mut Env<'a>,
response: WebResourceResponse,
) -> jni::errors::Result<JObject<'a>> {
let response_class = env.find_class(jni_str!(
"com/lingxia/webview/LingXiaWebView$WebResourceResponseData"
))?;
let (parts, body) = response.into_parts();
let status = parts.status.as_u16() as i32;
let reason = parts.status.canonical_reason().unwrap_or("Unknown");
let headers = &parts.headers;
let (mime_type, encoding) = headers
.get(http::header::CONTENT_TYPE)
.and_then(|h| h.to_str().ok())
.map(|content_type| {
let parts: Vec<&str> = content_type.split(';').map(str::trim).collect();
let mime = parts[0];
let enc = parts
.iter()
.find(|p| p.starts_with("charset="))
.map(|p| p.trim_start_matches("charset="))
.unwrap_or("UTF-8");
(mime, enc)
})
.unwrap_or(("application/octet-stream", "UTF-8"));
let map = env.new_object(jni_str!("java/util/HashMap"), jni_sig!("()V"), &[])?;
for (key, value) in headers.iter() {
if let Ok(v) = value.to_str() {
let key_str = env.new_string(key.as_str())?;
let value_str = env.new_string(v)?;
let _ = env.call_method(
&map,
jni_str!("put"),
jni_sig!("(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"),
&[(&key_str).into(), (&value_str).into()],
);
}
}
let mime_type_str = env.new_string(mime_type)?;
let encoding_str = env.new_string(encoding)?;
let reason_str = env.new_string(reason)?;
let (file_path_str, pipe_fd_jint, data_array, content_length): (JString, jint, JObject, i64) =
match body {
WebResourceBody::Path(path) => {
let file_path_str = env.new_string(path.to_string_lossy())?;
let content_length = headers
.get(http::header::CONTENT_LENGTH)
.and_then(|h| h.to_str().ok())
.and_then(|v| v.parse::<i64>().ok())
.or_else(|| fs::metadata(&path).ok().map(|meta| meta.len() as i64))
.unwrap_or(-1);
(file_path_str, 0, JObject::null(), content_length)
}
WebResourceBody::Pipe(reader) => {
let empty_path = env.new_string("")?;
let fd = reader.into_raw_fd();
(empty_path, fd as jint, JObject::null(), -1i64)
}
WebResourceBody::Bytes(data) => {
let empty_path = env.new_string("")?;
let data_array: JByteArray = env.byte_array_from_slice(&data)?;
(empty_path, 0, JObject::from(data_array), data.len() as i64)
}
};
env.new_object(
response_class,
jni_sig!("(Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/util/Map;Ljava/lang/String;I[BJ)V"),
&[
(&mime_type_str).into(),
(&encoding_str).into(),
status.into(),
(&reason_str).into(),
(&map).into(),
(&file_path_str).into(),
pipe_fd_jint.into(),
(&data_array).into(),
content_length.into(),
],
)
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_lingxia_webview_LingXiaWebView_handleNavigationPolicy(
mut env: EnvUnowned,
_this: JObject,
appid: JString,
path: JString,
session_id: jlong,
url: JString,
) -> bool {
env.with_env(|env| -> Result<bool, jni::errors::Error> {
let appid: String = appid.try_to_string(env)?;
let path: String = path.try_to_string(env)?;
let url: String = url.try_to_string(env)?;
let session_id = if session_id > 0 {
Some(session_id as u64)
} else {
None
};
let webtag = WebTag::new(&appid, &path, session_id);
if let Some(webview) = find_webview(&webtag) {
return Ok(matches!(
webview.handle_navigation(&url),
NavigationPolicy::Cancel
));
}
Ok(false)
})
.resolve::<ThrowRuntimeExAndDefault>()
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_lingxia_webview_LingXiaWebView_onConsoleMessage(
mut env: EnvUnowned,
_this: JObject,
appid: JString,
path: JString,
session_id: jlong,
level: jint,
message: JString,
) -> jint {
env.with_env(|env| -> Result<jint, jni::errors::Error> {
let appid: String = appid.try_to_string(env)?;
let path: String = path.try_to_string(env)?;
let message: String = message.try_to_string(env)?;
let session_id = if session_id > 0 {
Some(session_id as u64)
} else {
None
};
let webtag = WebTag::new(&appid, &path, session_id);
let log_level = match level {
2 => LogLevel::Verbose, 3 => LogLevel::Debug, 4 => LogLevel::Info, 5 => LogLevel::Warn, 6 => LogLevel::Error, _ => LogLevel::Info, };
if let Some(delegate) = find_webview_delegate(&webtag) {
delegate.log(log_level, &message);
}
Ok(1)
})
.resolve::<ThrowRuntimeExAndDefault>()
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_lingxia_webview_LingXiaWebView_notifyWebViewReady(
mut env: EnvUnowned,
_class: JObject,
appid: JString,
path: JString,
session_id: jlong,
webview_obj: JObject,
) {
env.with_env(|env| -> Result<(), jni::errors::Error> {
let appid: String = appid.try_to_string(env)?;
let path: String = path.try_to_string(env)?;
let session_id = if session_id > 0 {
Some(session_id as u64)
} else {
None
};
if let Some(senders) = WEBVIEW_SENDERS.get() {
let webtag = WebTag::new(&appid, &path, session_id);
let mut matched_pending = false;
if let Ok(mut senders_map) = senders.lock()
&& let Some(pending) = senders_map.remove(&webtag.to_string())
{
matched_pending = true;
match env.new_global_ref(webview_obj) {
Ok(global_ref) => {
let webview_inner =
WebViewInner::from_java_object(global_ref, webtag.clone());
let webview = Arc::new(crate::WebView::new(
webview_inner,
pending.effective_options.clone(),
));
register_webview(webview.clone());
pending.sender.succeed(webview);
}
Err(e) => {
pending.sender.fail(
WebViewCreateStage::Requested,
WebViewError::WebView(format!("Failed to create global ref: {:?}", e)),
);
}
}
}
if !matched_pending {
log::warn!(
"notifyWebViewReady without pending sender for {}",
webtag.as_str()
);
}
} else {
log::warn!(
"notifyWebViewReady called before sender map initialization for {}:{}",
appid,
path
);
}
Ok(())
})
.resolve::<ThrowRuntimeExAndDefault>()
}