use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::pin::Pin;
use std::sync::mpsc::{SyncSender, sync_channel};
use std::sync::{Arc, Mutex, OnceLock, RwLock};
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use tokio::sync::watch;
#[cfg(target_os = "android")]
use crate::android::WebViewInner;
#[cfg(any(target_os = "ios", target_os = "macos"))]
use crate::apple::WebViewInner;
#[cfg(all(target_os = "linux", target_env = "ohos"))]
use crate::harmony::WebViewInner;
use crate::traits::{
AsyncSchemeHandler, DownloadHandler, DownloadRequest, NavigationHandler, NavigationPolicy,
NewWindowHandler, NewWindowPolicy, SchemeOutcome,
};
use crate::{
LoadDataRequest, WebResourceResponse, WebViewController, WebViewDelegate, WebViewError,
};
fn lock_or_recover<'a, T>(mutex: &'a Mutex<T>, name: &str) -> std::sync::MutexGuard<'a, T> {
match mutex.lock() {
Ok(guard) => guard,
Err(poisoned) => {
log::error!("Mutex poisoned at {}, recovering inner value", name);
poisoned.into_inner()
}
}
}
fn scheme_waker_from_sender(sender: SyncSender<()>) -> Waker {
unsafe { Waker::from_raw(scheme_raw_waker(Arc::new(sender))) }
}
fn scheme_raw_waker(sender: Arc<SyncSender<()>>) -> RawWaker {
RawWaker::new(Arc::into_raw(sender) as *const (), &SCHEME_WAKER_VTABLE)
}
unsafe fn scheme_waker_clone(data: *const ()) -> RawWaker {
let arc = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
let cloned = Arc::clone(&arc);
let _ = Arc::into_raw(arc);
scheme_raw_waker(cloned)
}
unsafe fn scheme_waker_wake(data: *const ()) {
let arc = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
let _ = arc.try_send(());
}
unsafe fn scheme_waker_wake_by_ref(data: *const ()) {
let arc = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
let _ = arc.try_send(());
let _ = Arc::into_raw(arc);
}
unsafe fn scheme_waker_drop(data: *const ()) {
let _ = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
}
static SCHEME_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new(
scheme_waker_clone,
scheme_waker_wake,
scheme_waker_wake_by_ref,
scheme_waker_drop,
);
fn block_on_scheme_future<F>(future: F) -> F::Output
where
F: Future,
{
let (tx, rx) = sync_channel::<()>(1);
let waker = scheme_waker_from_sender(tx);
let mut context = Context::from_waker(&waker);
let mut future = Box::pin(future);
loop {
match Pin::as_mut(&mut future).poll(&mut context) {
Poll::Ready(value) => return value,
Poll::Pending => {
if rx.recv().is_err() {
std::thread::yield_now();
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum SecurityProfile {
StrictDefault,
BrowserRelaxed,
}
pub(crate) struct WebViewCreateOptions {
pub(crate) profile: SecurityProfile,
pub(crate) scheme_handlers: HashMap<String, AsyncSchemeHandler>,
pub(crate) navigation_handler: Option<NavigationHandler>,
pub(crate) new_window_handler: Option<NewWindowHandler>,
pub(crate) download_handler: Option<DownloadHandler>,
pub(crate) delegate: Option<Arc<dyn WebViewDelegate>>,
}
impl std::fmt::Debug for WebViewCreateOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WebViewCreateOptions")
.field("profile", &self.profile)
.field(
"scheme_handlers",
&self.scheme_handlers.keys().collect::<Vec<_>>(),
)
.field("has_navigation_handler", &self.navigation_handler.is_some())
.field("has_new_window_handler", &self.new_window_handler.is_some())
.field("has_download_handler", &self.download_handler.is_some())
.field("has_delegate", &self.delegate.is_some())
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProxyConfig {
pub host: String,
pub port: u16,
#[serde(default)]
pub bypass: Vec<String>,
}
impl ProxyConfig {
pub fn new(host: impl Into<String>, port: u16) -> Result<Self, WebViewError> {
let cfg = Self {
host: host.into(),
port,
bypass: Vec::new(),
};
cfg.validate()
}
pub fn with_bypass<I, S>(mut self, bypass: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.bypass = bypass.into_iter().map(Into::into).collect();
self
}
fn validate(self) -> Result<Self, WebViewError> {
let host = self.host.trim().to_string();
if host.is_empty() {
return Err(WebViewError::InvalidCreateOptions(
"proxy host cannot be empty".to_string(),
));
}
if host.contains(char::is_whitespace) {
return Err(WebViewError::InvalidCreateOptions(
"proxy host cannot contain whitespace".to_string(),
));
}
if self.port == 0 {
return Err(WebViewError::InvalidCreateOptions(
"proxy port must be greater than 0".to_string(),
));
}
let mut seen = HashSet::new();
let mut bypass = Vec::new();
for raw in self.bypass {
let rule = raw.trim();
if rule.is_empty() {
continue;
}
let key = rule.to_ascii_lowercase();
if seen.insert(key) {
bypass.push(rule.to_string());
}
}
Ok(Self {
host,
bypass,
..self
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProxyApplyStatus {
Applied,
Cleared,
Unsupported,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProxyActivation {
EffectiveNow,
NewWebViewsOnly,
EngineRecreateRequired,
NotApplied,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProxyApplyReport {
pub status: ProxyApplyStatus,
pub activation: ProxyActivation,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
impl ProxyApplyReport {
pub fn applied(activation: ProxyActivation) -> Self {
Self {
status: ProxyApplyStatus::Applied,
activation,
detail: None,
}
}
pub fn cleared(activation: ProxyActivation) -> Self {
Self {
status: ProxyApplyStatus::Cleared,
activation,
detail: None,
}
}
pub fn unsupported(detail: impl Into<String>) -> Self {
Self {
status: ProxyApplyStatus::Unsupported,
activation: ProxyActivation::NotApplied,
detail: Some(detail.into()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub(crate) struct EffectiveWebViewCreateOptions {
pub(crate) profile: SecurityProfile,
#[serde(default)]
pub(crate) registered_schemes: Vec<String>,
#[serde(default)]
pub(crate) has_navigation_handler: bool,
#[serde(default)]
pub(crate) has_new_window_handler: bool,
#[serde(default)]
pub(crate) has_download_handler: bool,
#[serde(default)]
pub(crate) has_delegate: bool,
}
impl Default for WebViewCreateOptions {
fn default() -> Self {
Self::strict()
}
}
impl WebViewCreateOptions {
fn strict() -> Self {
Self {
profile: SecurityProfile::StrictDefault,
scheme_handlers: HashMap::new(),
navigation_handler: None,
new_window_handler: None,
download_handler: None,
delegate: None,
}
}
fn browser() -> Self {
Self {
profile: SecurityProfile::BrowserRelaxed,
scheme_handlers: HashMap::new(),
navigation_handler: None,
new_window_handler: None,
download_handler: None,
delegate: None,
}
}
fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
where
F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
{
let normalized = scheme.trim().to_ascii_lowercase();
if !normalized.is_empty() {
self.scheme_handlers.insert(
normalized,
Arc::new(move |req| {
let fut = handler(req);
Box::pin(fut)
}),
);
}
self
}
fn on_navigation<F>(mut self, handler: F) -> Self
where
F: Fn(&str) -> NavigationPolicy + Send + Sync + 'static,
{
self.navigation_handler = Some(Box::new(handler));
self
}
fn on_new_window<F>(mut self, handler: F) -> Self
where
F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
{
self.new_window_handler = Some(Box::new(handler));
self
}
fn on_download<F>(mut self, handler: F) -> Self
where
F: Fn(DownloadRequest) + Send + Sync + 'static,
{
self.download_handler = Some(Box::new(handler));
self
}
fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
self.delegate = Some(delegate);
self
}
pub(crate) fn normalize(
self,
) -> Result<(EffectiveWebViewCreateOptions, PendingCallbacks), WebViewError> {
if self.profile != SecurityProfile::BrowserRelaxed && self.download_handler.is_some() {
return Err(WebViewError::InvalidCreateOptions(
"download callback is only supported in browser profile; use WebViewBuilder::browser(webtag).on_download(...).create()".to_string(),
));
}
let mut registered_schemes: Vec<String> = self.scheme_handlers.keys().cloned().collect();
registered_schemes.sort_unstable();
registered_schemes.dedup();
let effective = EffectiveWebViewCreateOptions {
profile: self.profile,
registered_schemes,
has_navigation_handler: self.navigation_handler.is_some(),
has_new_window_handler: self.new_window_handler.is_some(),
has_download_handler: self.download_handler.is_some(),
has_delegate: self.delegate.is_some(),
};
let pending = PendingCallbacks {
scheme_handlers: self.scheme_handlers,
navigation_handler: self.navigation_handler,
new_window_handler: self.new_window_handler,
download_handler: self.download_handler,
delegate: self.delegate,
};
Ok((effective, pending))
}
}
pub struct WebViewBuilder;
#[must_use = "call .create() to start WebView creation"]
pub struct StrictWebViewBuilder {
webtag: WebTag,
options: WebViewCreateOptions,
}
#[must_use = "call .create() to start WebView creation"]
pub struct BrowserWebViewBuilder {
webtag: WebTag,
options: WebViewCreateOptions,
}
impl WebViewBuilder {
#[must_use = "call .create() to start WebView creation"]
pub fn strict(webtag: WebTag) -> StrictWebViewBuilder {
StrictWebViewBuilder {
webtag,
options: WebViewCreateOptions::strict(),
}
}
#[must_use = "call .create() to start WebView creation"]
pub fn browser(webtag: WebTag) -> BrowserWebViewBuilder {
BrowserWebViewBuilder {
webtag,
options: WebViewCreateOptions::browser(),
}
}
}
impl StrictWebViewBuilder {
pub fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
self.options = self.options.delegate(delegate);
self
}
pub fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
where
F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
{
self.options = self.options.on_scheme(scheme, handler);
self
}
pub fn on_navigation<F>(mut self, handler: F) -> Self
where
F: Fn(&str) -> NavigationPolicy + Send + Sync + 'static,
{
self.options = self.options.on_navigation(handler);
self
}
pub fn on_new_window<F>(mut self, handler: F) -> Self
where
F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
{
self.options = self.options.on_new_window(handler);
self
}
pub fn create(self) -> WebViewSession {
create_webview_session(self.webtag, self.options)
}
}
impl BrowserWebViewBuilder {
pub fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
self.options = self.options.delegate(delegate);
self
}
pub fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
where
F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
{
self.options = self.options.on_scheme(scheme, handler);
self
}
pub fn on_navigation<F>(mut self, handler: F) -> Self
where
F: Fn(&str) -> NavigationPolicy + Send + Sync + 'static,
{
self.options = self.options.on_navigation(handler);
self
}
pub fn on_new_window<F>(mut self, handler: F) -> Self
where
F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
{
self.options = self.options.on_new_window(handler);
self
}
pub fn on_download<F>(mut self, handler: F) -> Self
where
F: Fn(DownloadRequest) + Send + Sync + 'static,
{
self.options = self.options.on_download(handler);
self
}
pub fn create(self) -> WebViewSession {
create_webview_session(self.webtag, self.options)
}
}
pub(crate) struct PendingCallbacks {
pub(crate) scheme_handlers: HashMap<String, AsyncSchemeHandler>,
pub(crate) navigation_handler: Option<NavigationHandler>,
pub(crate) new_window_handler: Option<NewWindowHandler>,
pub(crate) download_handler: Option<DownloadHandler>,
pub(crate) delegate: Option<Arc<dyn WebViewDelegate>>,
}
impl PendingCallbacks {
fn has_any(&self) -> bool {
!self.scheme_handlers.is_empty()
|| self.navigation_handler.is_some()
|| self.new_window_handler.is_some()
|| self.download_handler.is_some()
|| self.delegate.is_some()
}
}
pub struct WebView {
pub(crate) inner: WebViewInner,
effective_options: EffectiveWebViewCreateOptions,
delegate: RwLock<Option<Arc<dyn WebViewDelegate>>>,
scheme_handlers: RwLock<HashMap<String, AsyncSchemeHandler>>,
navigation_handler: RwLock<Option<NavigationHandler>>,
new_window_handler: RwLock<Option<NewWindowHandler>>,
download_handler: RwLock<Option<DownloadHandler>>,
}
impl WebView {
pub(crate) fn new(
inner: WebViewInner,
effective_options: EffectiveWebViewCreateOptions,
) -> Self {
Self {
inner,
effective_options,
delegate: RwLock::new(None),
scheme_handlers: RwLock::new(HashMap::new()),
navigation_handler: RwLock::new(None),
new_window_handler: RwLock::new(None),
download_handler: RwLock::new(None),
}
}
pub fn appid(&self) -> String {
self.inner.webtag.extract_appid()
}
pub fn path(&self) -> String {
self.inner.webtag.extract_parts().1
}
pub fn webtag(&self) -> WebTag {
self.inner.webtag.clone()
}
pub(crate) fn effective_options(&self) -> &EffectiveWebViewCreateOptions {
&self.effective_options
}
pub(crate) fn get_delegate(&self) -> Option<Arc<dyn WebViewDelegate>> {
self.delegate.read().ok().and_then(|guard| guard.clone())
}
pub(crate) fn remove_delegate(&self) {
if let Ok(mut guard) = self.delegate.write() {
*guard = None;
}
}
pub(crate) fn install_callbacks(&self, callbacks: PendingCallbacks) {
if let Some(delegate) = callbacks.delegate
&& let Ok(mut guard) = self.delegate.write()
{
*guard = Some(delegate);
}
if let Ok(mut guard) = self.scheme_handlers.write() {
*guard = callbacks.scheme_handlers;
}
if let Some(handler) = callbacks.navigation_handler
&& let Ok(mut guard) = self.navigation_handler.write()
{
*guard = Some(handler);
}
if let Some(handler) = callbacks.new_window_handler
&& let Ok(mut guard) = self.new_window_handler.write()
{
*guard = Some(handler);
}
if let Some(handler) = callbacks.download_handler
&& let Ok(mut guard) = self.download_handler.write()
{
*guard = Some(handler);
}
}
pub fn has_scheme_handler(&self, scheme: &str) -> bool {
self.scheme_handlers
.read()
.ok()
.is_some_and(|guard| guard.contains_key(scheme))
}
pub(crate) fn handle_scheme_request(
&self,
scheme: &str,
request: http::Request<Vec<u8>>,
) -> Option<WebResourceResponse> {
#[cfg(any(target_os = "ios", target_os = "macos"))]
if let Some(response) = self.inner.handle_internal_bridge_request(&request) {
return Some(response);
}
let guard = self.scheme_handlers.read().ok()?;
let handler = guard.get(scheme)?;
let outcome = block_on_scheme_future(handler(request));
match outcome {
SchemeOutcome::Handled(response) => Some(response),
SchemeOutcome::PassThrough => None,
}
}
pub fn handle_navigation(&self, url: &str) -> NavigationPolicy {
if let Ok(guard) = self.navigation_handler.read()
&& let Some(handler) = guard.as_ref()
{
return handler(url);
}
NavigationPolicy::Allow
}
pub fn has_new_window_handler(&self) -> bool {
self.new_window_handler
.read()
.ok()
.is_some_and(|guard| guard.is_some())
}
pub fn handle_new_window(&self, url: &str) -> NewWindowPolicy {
if let Ok(guard) = self.new_window_handler.read()
&& let Some(handler) = guard.as_ref()
{
return handler(url);
}
NewWindowPolicy::Cancel
}
pub(crate) fn handle_download(&self, request: DownloadRequest) {
if let Ok(guard) = self.download_handler.read()
&& let Some(handler) = guard.as_ref()
{
handler(request);
}
}
#[cfg(target_os = "macos")]
pub fn toggle_devtools(&self) {
self.inner.toggle_devtools();
}
#[cfg(target_os = "macos")]
pub fn toggle_devtools_detached(&self) {
self.inner.toggle_devtools_detached();
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub fn get_swift_webview_ptr(&self) -> usize {
self.inner.get_swift_webview_ptr()
}
#[cfg(target_os = "android")]
pub fn get_java_webview(&self) -> &jni::objects::Global<jni::objects::JObject<'static>> {
self.inner.get_java_webview()
}
}
impl WebViewController for WebView {
fn load_url(&self, url: &str) -> Result<(), WebViewError> {
self.inner.load_url(url)
}
fn load_data(&self, request: LoadDataRequest<'_>) -> Result<(), WebViewError> {
self.inner.load_data(request)
}
fn evaluate_javascript(&self, js: &str) -> Result<(), WebViewError> {
self.inner.evaluate_javascript(js)
}
fn post_message(&self, message: &str) -> Result<(), WebViewError> {
self.inner.post_message(message)
}
fn clear_browsing_data(&self) -> Result<(), WebViewError> {
self.inner.clear_browsing_data()
}
fn set_user_agent(&self, ua: &str) -> Result<(), WebViewError> {
self.inner.set_user_agent(ua)
}
}
type WebViewInstancesMap = Arc<Mutex<HashMap<String, Arc<WebView>>>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WebViewCreateStage {
Requested,
NativeCreated,
ControllerAttached,
Ready,
Destroyed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WebViewEvent {
Stage(WebViewCreateStage),
Failed {
stage: WebViewCreateStage,
error: WebViewError,
},
}
type WebViewReadyState = Option<Result<Arc<WebView>, WebViewError>>;
#[derive(Clone)]
pub struct WebViewEventSubscription {
rx: watch::Receiver<WebViewEvent>,
}
impl WebViewEventSubscription {
pub fn current(&self) -> WebViewEvent {
self.rx.borrow().clone()
}
pub async fn changed(&mut self) -> Result<WebViewEvent, WebViewError> {
self.rx.changed().await.map_err(|_| {
WebViewError::WebView("webview event channel unexpectedly closed".to_string())
})?;
Ok(self.current())
}
}
#[derive(Clone)]
pub struct WebViewSession {
webtag: WebTag,
event_rx: watch::Receiver<WebViewEvent>,
ready_rx: watch::Receiver<WebViewReadyState>,
signals: Arc<WebViewSessionSignals>,
}
impl WebViewSession {
pub fn webtag(&self) -> &WebTag {
&self.webtag
}
pub fn subscribe_events(&self) -> WebViewEventSubscription {
WebViewEventSubscription {
rx: self.event_rx.clone(),
}
}
pub fn current_event(&self) -> WebViewEvent {
self.event_rx.borrow().clone()
}
pub async fn wait_ready(&self) -> Result<Arc<WebView>, WebViewError> {
let mut rx = self.ready_rx.clone();
loop {
if let Some(result) = self.signals.terminal_result() {
return result;
}
if let Some(result) = rx.borrow().clone() {
return result;
}
if rx.changed().await.is_err() {
if let Some(result) = self.signals.terminal_result() {
return result;
}
return Err(WebViewError::WebView(
"webview ready channel unexpectedly closed".to_string(),
));
}
}
}
}
struct WebViewSessionSignals {
event_tx: watch::Sender<WebViewEvent>,
ready_tx: watch::Sender<WebViewReadyState>,
state: Mutex<WebViewSessionState>,
}
#[derive(Default)]
struct WebViewSessionState {
terminal_result: Option<Result<Arc<WebView>, WebViewError>>,
destroyed: bool,
}
impl WebViewSessionSignals {
fn new() -> Arc<Self> {
let (event_tx, _event_rx) =
watch::channel(WebViewEvent::Stage(WebViewCreateStage::Requested));
let (ready_tx, _ready_rx) = watch::channel(None);
Arc::new(Self {
event_tx,
ready_tx,
state: Mutex::new(WebViewSessionState::default()),
})
}
fn subscribe(self: &Arc<Self>, webtag: WebTag) -> WebViewSession {
WebViewSession {
webtag,
event_rx: self.event_tx.subscribe(),
ready_rx: self.ready_tx.subscribe(),
signals: Arc::clone(self),
}
}
fn terminal_result(&self) -> Option<Result<Arc<WebView>, WebViewError>> {
let state = lock_or_recover(&self.state, "webview_session_state.terminal_result");
state.terminal_result.clone()
}
fn publish_result(
&self,
result: Result<Arc<WebView>, WebViewError>,
stage_on_error: WebViewCreateStage,
) {
let mut state = lock_or_recover(&self.state, "webview_session_state.publish_result");
if state.destroyed || state.terminal_result.is_some() {
return;
}
state.terminal_result = Some(result.clone());
drop(state);
match result {
Ok(webview) => {
self.event_tx
.send_replace(WebViewEvent::Stage(WebViewCreateStage::NativeCreated));
self.event_tx
.send_replace(WebViewEvent::Stage(WebViewCreateStage::ControllerAttached));
self.ready_tx.send_replace(Some(Ok(webview)));
self.event_tx
.send_replace(WebViewEvent::Stage(WebViewCreateStage::Ready));
}
Err(error) => {
self.ready_tx.send_replace(Some(Err(error.clone())));
self.event_tx.send_replace(WebViewEvent::Failed {
stage: stage_on_error,
error,
});
}
}
}
fn publish_destroyed(&self) {
let mut state = lock_or_recover(&self.state, "webview_session_state.publish_destroyed");
if state.destroyed {
return;
}
state.destroyed = true;
if state.terminal_result.is_none() {
state.terminal_result = Some(Err(WebViewError::WebView(
"webview destroyed before ready".to_string(),
)));
}
let terminal_result = state.terminal_result.clone();
drop(state);
self.event_tx
.send_replace(WebViewEvent::Stage(WebViewCreateStage::Destroyed));
if let Some(result) = terminal_result {
self.ready_tx.send_replace(Some(result));
}
}
}
pub(crate) struct WebViewCreateSender {
signals: Arc<WebViewSessionSignals>,
}
impl WebViewCreateSender {
fn new(signals: Arc<WebViewSessionSignals>) -> Self {
Self { signals }
}
pub(crate) fn succeed(self, webview: Arc<WebView>) {
self.signals
.publish_result(Ok(webview), WebViewCreateStage::Requested);
}
pub(crate) fn fail(self, stage: WebViewCreateStage, error: WebViewError) {
self.signals.publish_result(Err(error), stage);
}
}
static WEBVIEW_INSTANCES: OnceLock<WebViewInstancesMap> = OnceLock::new();
static PENDING_CALLBACKS: OnceLock<Mutex<HashMap<String, PendingCallbacks>>> = OnceLock::new();
static WEBVIEW_SESSIONS: OnceLock<Mutex<HashMap<String, Arc<WebViewSessionSignals>>>> =
OnceLock::new();
static CURRENT_PROXY: OnceLock<RwLock<Option<ProxyConfig>>> = OnceLock::new();
static PROXY_APPLY_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
fn apply_http_proxy_platform(
config: Option<&ProxyConfig>,
) -> Result<ProxyApplyReport, WebViewError> {
#[cfg(target_os = "android")]
{
return crate::android::apply_http_proxy(config);
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
{
return crate::apple::apply_http_proxy(config);
}
#[cfg(all(target_os = "linux", target_env = "ohos"))]
{
return crate::harmony::apply_http_proxy(config);
}
#[cfg(not(any(
target_os = "android",
target_os = "ios",
target_os = "macos",
all(target_os = "linux", target_env = "ohos")
)))]
{
let _ = config;
Ok(ProxyApplyReport::unsupported(
"proxy is not supported on this platform",
))
}
}
pub fn set_proxy(config: Option<ProxyConfig>) -> Result<ProxyApplyReport, WebViewError> {
let apply_lock = PROXY_APPLY_LOCK.get_or_init(|| Mutex::new(()));
let _guard = lock_or_recover(apply_lock, "webview_proxy_apply_lock");
let normalized_config = match config {
Some(cfg) => Some(cfg.validate()?),
None => None,
};
let report = apply_http_proxy_platform(normalized_config.as_ref())?;
if matches!(
report.status,
ProxyApplyStatus::Applied | ProxyApplyStatus::Cleared
) {
let state = CURRENT_PROXY.get_or_init(|| RwLock::new(None));
match state.write() {
Ok(mut guard) => {
*guard = normalized_config;
}
Err(poisoned) => {
log::error!("RwLock poisoned at webview_current_proxy.write, recovering");
*poisoned.into_inner() = normalized_config;
}
}
}
Ok(report)
}
pub fn current_proxy() -> Option<ProxyConfig> {
let state = CURRENT_PROXY.get()?;
match state.read() {
Ok(guard) => guard.clone(),
Err(poisoned) => {
log::error!("RwLock poisoned at webview_current_proxy.read, recovering");
poisoned.into_inner().clone()
}
}
}
fn clear_pending_callbacks(webtag: &WebTag) {
if let Some(pending) = PENDING_CALLBACKS.get()
&& let Ok(mut map) = pending.lock()
{
map.remove(webtag.key());
}
}
fn replace_session_signals(webtag: &WebTag, signals: Arc<WebViewSessionSignals>) {
let sessions = WEBVIEW_SESSIONS.get_or_init(|| Mutex::new(HashMap::new()));
let mut guard = lock_or_recover(sessions, "webview_sessions.replace");
guard.insert(webtag.key().to_string(), signals);
}
fn remove_session_signals(webtag: &WebTag) -> Option<Arc<WebViewSessionSignals>> {
let sessions = WEBVIEW_SESSIONS.get()?;
let mut guard = lock_or_recover(sessions, "webview_sessions.remove");
guard.remove(webtag.key())
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WebTag(String);
impl std::fmt::Display for WebTag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl WebTag {
pub fn new(appid: &str, path: &str, session_id: Option<u64>) -> Self {
let mut tag = format!("{}:{}", appid, path);
if let Some(session) = session_id {
tag.push('#');
tag.push_str(&session.to_string());
}
Self(tag)
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn key(&self) -> &str {
&self.0
}
pub fn extract_appid(&self) -> String {
self.0.split(':').next().unwrap_or("").to_string()
}
pub fn extract_parts(&self) -> (String, String) {
if let Some((appid, path_with_session)) = self.0.split_once(':') {
let path = path_with_session
.split('#')
.next()
.unwrap_or(path_with_session);
(appid.to_string(), path.to_string())
} else {
log::error!("Invalid webtag format: {}", self.0);
("".to_string(), self.0.clone())
}
}
pub fn session_id(&self) -> Option<u64> {
self.0
.split('#')
.nth(1)
.and_then(|raw| raw.parse::<u64>().ok())
}
}
impl From<&str> for WebTag {
fn from(webtag_str: &str) -> Self {
Self(webtag_str.to_string())
}
}
fn request_create_webview(
webtag: &WebTag,
sender: WebViewCreateSender,
options: WebViewCreateOptions,
) {
let (appid, path) = webtag.extract_parts();
let (effective_options, pending_callbacks) = match options.normalize() {
Ok(value) => value,
Err(error) => {
sender.fail(WebViewCreateStage::Requested, error);
return;
}
};
log::info!(
"Creating WebView for key={} profile={:?} schemes={:?}",
webtag.key(),
effective_options.profile,
effective_options.registered_schemes,
);
let instances = WEBVIEW_INSTANCES.get_or_init(|| Arc::new(Mutex::new(HashMap::new())));
if let Ok(webviews) = instances.lock()
&& let Some(existing_webview) = webviews.get(webtag.key())
{
if existing_webview.effective_options() != &effective_options {
sender.fail(
WebViewCreateStage::Requested,
WebViewError::InvalidCreateOptions(format!(
"webview already exists with different options: key={} existing={:?} requested={:?}",
webtag.key(),
existing_webview.effective_options(),
effective_options
)),
);
return;
}
if pending_callbacks.has_any() {
sender.fail(
WebViewCreateStage::Requested,
WebViewError::InvalidCreateOptions(format!(
"webview already exists and callback registrations are immutable: key={} options={:?}",
webtag.key(),
existing_webview.effective_options()
)),
);
log::warn!(
"Rejected recreate with callbacks for existing webview key={} options={:?}",
webtag.key(),
existing_webview.effective_options()
);
return;
}
log::info!("WebView already exists, reusing: {}", webtag.key());
sender.succeed(existing_webview.clone());
return;
}
clear_pending_callbacks(webtag);
if pending_callbacks.has_any() {
let pending = PENDING_CALLBACKS.get_or_init(|| Mutex::new(HashMap::new()));
if let Ok(mut map) = pending.lock() {
map.insert(webtag.key().to_string(), pending_callbacks);
}
}
WebViewInner::create(
&appid,
&path,
webtag.session_id(),
effective_options,
sender,
);
}
fn create_webview_session(webtag: WebTag, options: WebViewCreateOptions) -> WebViewSession {
let signals = WebViewSessionSignals::new();
let session = signals.subscribe(webtag.clone());
let sender = WebViewCreateSender::new(signals.clone());
replace_session_signals(&webtag, signals);
request_create_webview(&webtag, sender, options);
session
}
pub(crate) fn register_webview(webview: Arc<WebView>) {
let webtag = webview.webtag();
if let Some(pending) = PENDING_CALLBACKS.get()
&& let Ok(mut map) = pending.lock()
&& let Some(callbacks) = map.remove(webtag.key())
{
log::info!(
"Installing callbacks for {} (schemes={}, nav={}, new_window={}, download={}, delegate={})",
webtag.key(),
callbacks.scheme_handlers.len(),
callbacks.navigation_handler.is_some(),
callbacks.new_window_handler.is_some(),
callbacks.download_handler.is_some(),
callbacks.delegate.is_some()
);
webview.install_callbacks(callbacks);
}
if let Some(instances) = WEBVIEW_INSTANCES.get()
&& let Ok(mut webviews) = instances.lock()
{
webviews.insert(webtag.key().to_string(), webview.clone());
log::info!("WebView created and stored: {}", webtag.key());
}
}
pub(crate) fn find_webview(webtag: &WebTag) -> Option<Arc<WebView>> {
if let Some(instances) = WEBVIEW_INSTANCES.get() {
if let Ok(webviews) = instances.lock() {
webviews.get(webtag.key()).cloned()
} else {
None
}
} else {
None
}
}
#[cfg(any(
target_os = "android",
target_os = "ios",
target_os = "macos",
all(target_os = "linux", target_env = "ohos")
))]
pub(crate) fn find_webview_delegate(webtag: &WebTag) -> Option<Arc<dyn WebViewDelegate>> {
find_webview(webtag).and_then(|webview| webview.get_delegate())
}
pub(crate) fn destroy_webview(webtag: &WebTag) {
let removed = if let Some(instances) = WEBVIEW_INSTANCES.get()
&& let Ok(mut webviews) = instances.lock()
{
webviews.remove(webtag.key())
} else {
None
};
if let Some(webview) = removed {
webview.remove_delegate();
}
clear_pending_callbacks(webtag);
if let Some(signals) = remove_session_signals(webtag) {
signals.publish_destroyed();
}
}