Skip to main content

lingxia_browser/
lib.rs

1#[cfg(all(test, target_vendor = "apple"))]
2#[path = "../../lingxia-platform/tests/support/apple_host_stubs.rs"]
3mod apple_host_stubs;
4
5mod automation;
6mod chooser;
7mod document_session;
8mod downloads;
9mod inbound;
10mod internal_pages;
11mod policy;
12mod tabs;
13mod types;
14mod webview;
15
16pub use lingxia_webview::{
17    NetworkBody, NetworkCaptureSnapshot, NetworkEntry, WebViewCookie, WebViewCookieSameSite,
18    WebViewCookieSetRequest,
19};
20pub use policy::{extract_url_scheme, is_lingxia_startup_url, normalize_url_for_wait_compare};
21use std::sync::Arc;
22use std::time::Duration;
23pub use types::{
24    BrowserAddressAction, BrowserAddressInputContext, BrowserAddressInputError,
25    BrowserAddressInputRequest, BrowserAddressInputResponse, BrowserAddressInputTrigger,
26    BrowserAddressNavigation, BrowserAddressState, BrowserAddressSuggestion,
27    BrowserAddressValueKind, BrowserAutomationError, BrowserElementInfo, BrowserNativeInputHost,
28    BrowserNavigationPolicyDecision, BrowserNavigationPolicyRequest,
29    BrowserNavigationPolicyResponse, BrowserNavigationTarget, BrowserRect, BrowserTabInfo,
30    BrowserWaitCondition, BrowserWaitResult, TrustedControlPageNavigation,
31};
32
33pub use lxapp::LxAppError;
34
35pub const BUILTIN_BROWSER_APPID: &str = "app.lingxia.browser";
36/// Exact browser webui-to-host control protocol supported by this runtime.
37pub const CONTROL_PROTOCOL_VERSION: u32 = 3;
38/// `(url, title)` observer; `title` is empty when no title has been reported
39/// for that URL yet.
40pub type BrowserPageMetadataHandler = Arc<dyn Fn(&str, &str) + Send + Sync>;
41
42pub fn classify_navigation(
43    request: BrowserNavigationPolicyRequest,
44) -> BrowserNavigationPolicyResponse {
45    policy::handle_browser_navigation_policy(request)
46}
47
48pub fn classify_navigation_json(request_json: &str) -> Option<String> {
49    policy::handle_browser_navigation_policy_json(request_json)
50}
51
52#[doc(hidden)]
53pub fn set_navigation_finished_handler(handler: BrowserPageMetadataHandler) {
54    tabs::set_navigation_finished_handler(handler);
55}
56
57#[doc(hidden)]
58pub fn set_title_changed_handler(handler: BrowserPageMetadataHandler) {
59    tabs::set_title_changed_handler(handler);
60}
61
62#[doc(hidden)]
63pub fn register_document_script(
64    native_authority: &lxapp::NativeControlPlaneAuthority,
65    js: impl Into<String>,
66) -> Result<(), LxAppError> {
67    require_native_control_authority(native_authority)?;
68    internal_pages::register_browser_document_script(js)
69}
70
71#[doc(hidden)]
72pub fn install_runtime() {
73    lxapp::register_page_resolver(internal_pages::browser_logic_page_path_for_tab_path);
74    lingxia_transfer::runtime::register_browser_tab_path_resolver(tabs::browser_tab_path_for_id);
75    lingxia_transfer::runtime::register_browser_retry_handler(
76        downloads::retry_browser_owned_download,
77    );
78}
79
80/// Seal the native authority used by browser document lifecycle operations.
81/// Duplicate installation is rejected; there is no getter.
82#[doc(hidden)]
83pub fn __install_native_control_authority(authority: lxapp::NativeControlPlaneAuthority) -> bool {
84    document_session::install_browser_document_authority(authority)
85}
86
87#[doc(hidden)]
88pub fn register_internal_page(
89    native_authority: &lxapp::NativeControlPlaneAuthority,
90    route: impl Into<String>,
91    entry_asset: impl Into<String>,
92) -> Result<(), LxAppError> {
93    require_native_control_authority(native_authority)?;
94    internal_pages::register_browser_internal_page(route, entry_asset)
95}
96
97pub fn open(url: &str, tab_id: Option<&str>) -> Result<String, LxAppError> {
98    reject_trusted_control_url(url)?;
99    tabs::open_internal_browser_tab(url, tab_id)
100}
101
102#[doc(hidden)]
103pub fn open_trusted(
104    native_authority: &lxapp::NativeControlPlaneAuthority,
105    url: &str,
106    tab_id: Option<&str>,
107) -> Result<String, LxAppError> {
108    require_native_control_authority(native_authority)?;
109    tabs::open_internal_browser_tab(url, tab_id)
110}
111
112#[doc(hidden)]
113pub fn open_trusted_for_app(
114    native_authority: &lxapp::NativeControlPlaneAuthority,
115    appid: &str,
116    session_id: u64,
117    url: &str,
118    tab_id: Option<&str>,
119) -> Result<String, LxAppError> {
120    require_native_control_authority(native_authority)?;
121    tabs::open_internal_browser_tab_for_owner(
122        appid,
123        session_id,
124        url,
125        tab_id,
126        false,
127        false,
128        lingxia_webview::WebViewDataMode::ProfileDefault,
129        false,
130    )
131}
132
133/// Bootstrap-TCB entry that always requests a new trusted top-level load of a
134/// registered internal control page. It never returns document authority.
135#[doc(hidden)]
136pub fn navigate_trusted_control_page(
137    native_authority: &lxapp::NativeControlPlaneAuthority,
138    url: &str,
139) -> Result<TrustedControlPageNavigation, LxAppError> {
140    require_native_control_authority(native_authority)?;
141    tabs::navigate_trusted_control_page(url)
142}
143
144fn require_native_control_authority(
145    native_authority: &lxapp::NativeControlPlaneAuthority,
146) -> Result<(), LxAppError> {
147    native_authority.is_live().then_some(()).ok_or_else(|| {
148        LxAppError::UnsupportedOperation(
149            "trusted browser control operation requires live native host authority".to_string(),
150        )
151    })
152}
153
154fn reject_trusted_control_url(url: &str) -> Result<(), LxAppError> {
155    (extract_url_scheme(url).as_deref() != Some("lingxia"))
156        .then_some(())
157        .ok_or_else(|| {
158            LxAppError::UnsupportedOperation(
159                "lingxia:// navigation requires sealed native browser authority".to_string(),
160            )
161        })
162}
163
164#[doc(hidden)]
165pub fn seal_control_registration(
166    native_authority: &lxapp::NativeControlPlaneAuthority,
167) -> Result<(), LxAppError> {
168    require_native_control_authority(native_authority)?;
169    internal_pages::seal_browser_control_registration();
170    Ok(())
171}
172
173pub fn open_for_app(
174    appid: &str,
175    session_id: u64,
176    url: &str,
177    tab_id: Option<&str>,
178) -> Result<String, LxAppError> {
179    reject_trusted_control_url(url)?;
180    tabs::open_internal_browser_tab_for_owner(
181        appid,
182        session_id,
183        url,
184        tab_id,
185        false,
186        false,
187        lingxia_webview::WebViewDataMode::ProfileDefault,
188        false,
189    )
190}
191
192/// Open a tab in the API-managed aside browser group.
193pub fn open_aside_for_app(
194    appid: &str,
195    session_id: u64,
196    url: &str,
197    tab_id: Option<&str>,
198) -> Result<String, LxAppError> {
199    reject_trusted_control_url(url)?;
200    tabs::open_internal_browser_tab_for_owner(
201        appid,
202        session_id,
203        url,
204        tab_id,
205        false,
206        true,
207        lingxia_webview::WebViewDataMode::ProfileDefault,
208        false,
209    )
210}
211
212/// Open a standalone browser tab hosted outside the product browser chrome.
213/// This supports docked asides and URL surfaces. New-window requests load
214/// inline in the same WebView rather than spawning a main-area tab.
215pub fn open_standalone_for_app(
216    appid: &str,
217    session_id: u64,
218    url: &str,
219    tab_id: Option<&str>,
220    data_mode: lingxia_webview::WebViewDataMode,
221    url_callback: bool,
222) -> Result<String, LxAppError> {
223    reject_trusted_control_url(url)?;
224    tabs::open_internal_browser_tab_for_owner(
225        appid,
226        session_id,
227        url,
228        tab_id,
229        true,
230        false,
231        data_mode,
232        url_callback,
233    )
234}
235
236/// Whether `tab_id` belongs to the API-managed aside browser group.
237pub fn tab_is_aside(tab_id: &str) -> bool {
238    tabs::is_aside_tab(tab_id)
239}
240
241/// Whether `tab_id` is hosted outside the product browser chrome, such as a
242/// docked aside or URL surface. It remains visible to browser automation.
243pub fn tab_is_standalone(tab_id: &str) -> bool {
244    tabs::is_standalone_tab(tab_id)
245}
246
247pub fn close(tab_id: &str) -> Result<(), LxAppError> {
248    let owner_appid = tabs::tab_owner_appid(tab_id);
249    tabs::close_browser_tab(tab_id)?;
250    if let Some(owner_appid) = owner_appid {
251        let payload = serde_json::json!({
252            "id": tab_id,
253            "reason": "user",
254        })
255        .to_string();
256        let _ =
257            lxapp::publish_app_event(&owner_appid, lxapp::BROWSER_TAB_CLOSED_EVENT, Some(payload));
258    }
259    Ok(())
260}
261
262/// Retire browser tabs owned by previous sessions of an lxapp.
263pub fn prune_stale_owner_tabs(owner_appid: &str, current_session_id: u64) -> usize {
264    tabs::prune_stale_owner_tabs(owner_appid, current_session_id)
265}
266
267/// Discard a tab's WebView to free memory while keeping its sidebar entry.
268pub fn discard(tab_id: &str) -> Result<(), LxAppError> {
269    tabs::discard_browser_tab(tab_id)
270}
271
272/// Recreate a discarded tab's WebView, reload its URL, and activate it.
273pub fn reactivate(tab_id: &str) -> Result<(), LxAppError> {
274    tabs::reactivate_browser_tab(tab_id)
275}
276
277/// Sync the Rust-side active tab when the SDK switches to an already-live tab.
278pub fn mark_active(tab_id: &str) {
279    tabs::mark_browser_tab_active(tab_id)
280}
281
282/// Clear active browser state when the host leaves browser UI entirely.
283pub fn clear_active() {
284    tabs::clear_active_browser_tab()
285}
286
287pub fn tabs() -> Vec<BrowserTabInfo> {
288    tabs::browser_tabs()
289}
290
291pub fn current_tab() -> Option<BrowserTabInfo> {
292    tabs::browser_current_tab()
293}
294
295/// Current tab selected by browser automation. Standalone surface tabs can be
296/// selected here without changing the product browser's active tab.
297pub fn automation_current_tab() -> Option<BrowserTabInfo> {
298    tabs::browser_automation_current_tab()
299}
300
301pub fn activate(tab_id: &str) -> Result<BrowserTabInfo, BrowserAutomationError> {
302    tabs::browser_activate_tab(tab_id)
303}
304
305/// Registers a process-wide observer invoked whenever the browser tab set
306/// or tab metadata changes: tab opened/closed, active tab switched, or a
307/// tab's URL/title updated. Intended for shell UIs that mirror the tab
308/// list (e.g. sidebar tab rows); the previous handler (if any) is replaced.
309///
310/// The callback may fire from arbitrary runtime threads (webview UI
311/// threads included) and must not block; query [`tabs`]/[`current_tab`]
312/// from it to read the new state.
313pub fn set_tabs_changed_handler(handler: Arc<dyn Fn() + Send + Sync>) {
314    tabs::set_tabs_changed_handler(handler);
315}
316
317/// Registers a process-wide observer used to bring a browser tab onscreen.
318///
319/// Browser core owns tab lifecycle and active-tab state, but only a host shell
320/// knows how to present that tab's WebView in its UI. Devtools and other
321/// automation surfaces call [`present`] to request this handoff.
322pub fn set_tab_present_handler(handler: Arc<dyn Fn(&str) + Send + Sync>) {
323    tabs::set_tab_present_handler(handler);
324}
325
326/// Mark `tab_id` active and ask the host shell to bring it onscreen.
327pub fn present(tab_id: &str) -> Result<BrowserTabInfo, BrowserAutomationError> {
328    tabs::browser_present_tab(tab_id)
329}
330
331/// PNG-encoded favicon of `tab_id`'s current page, if the platform webview
332/// reported one (see `WebViewDelegate::on_favicon_changed`). Kept out of
333/// [`BrowserTabInfo`] so the serialized tab projection stays byte-free;
334/// shell sidebars query it per tab when mirroring the tab list.
335pub fn tab_favicon(tab_id: &str) -> Option<Arc<Vec<u8>>> {
336    tabs::browser_tab_favicon(tab_id)
337}
338
339pub fn register_native_input_host(host: Arc<dyn BrowserNativeInputHost>) -> bool {
340    automation::register_native_input_host(host)
341}
342
343pub async fn evaluate_javascript(
344    tab_id: &str,
345    js: &str,
346) -> Result<serde_json::Value, BrowserAutomationError> {
347    automation::browser_evaluate_javascript(tab_id, js).await
348}
349
350pub async fn take_screenshot(tab_id: &str) -> Result<Vec<u8>, BrowserAutomationError> {
351    automation::browser_take_screenshot(tab_id).await
352}
353
354pub async fn current_url(tab_id: &str) -> Result<Option<String>, BrowserAutomationError> {
355    automation::browser_current_url(tab_id).await
356}
357
358pub fn reload(tab_id: &str) -> Result<(), BrowserAutomationError> {
359    automation::browser_reload(tab_id)
360}
361
362/// Return the complete browser-session user-agent override. `None` means the
363/// platform WebView default is in use.
364pub fn configured_user_agent() -> Option<String> {
365    automation::browser_configured_user_agent()
366}
367
368/// Configure a complete user-agent string for all browser tabs, or pass `None`
369/// to restore the platform WebView default. The setting also applies to future
370/// and recreated browser WebViews for the lifetime of the process.
371pub fn set_user_agent_override(user_agent: Option<String>) -> Result<(), BrowserAutomationError> {
372    automation::browser_set_user_agent_override(user_agent)
373}
374
375pub fn go_back(tab_id: &str) -> Result<(), BrowserAutomationError> {
376    automation::browser_go_back(tab_id)
377}
378
379pub fn go_forward(tab_id: &str) -> Result<(), BrowserAutomationError> {
380    automation::browser_go_forward(tab_id)
381}
382
383pub async fn list_cookies(tab_id: &str) -> Result<Vec<WebViewCookie>, BrowserAutomationError> {
384    automation::browser_list_cookies(tab_id).await
385}
386
387pub async fn list_all_cookies(tab_id: &str) -> Result<Vec<WebViewCookie>, BrowserAutomationError> {
388    automation::browser_list_all_cookies(tab_id).await
389}
390
391pub async fn set_cookie(
392    tab_id: &str,
393    request: WebViewCookieSetRequest,
394) -> Result<(), BrowserAutomationError> {
395    automation::browser_set_cookie(tab_id, request).await
396}
397
398pub async fn delete_cookie(
399    tab_id: &str,
400    name: &str,
401    domain: &str,
402    path: &str,
403) -> Result<(), BrowserAutomationError> {
404    automation::browser_delete_cookie(tab_id, name, domain, path).await
405}
406
407pub async fn clear_cookies(tab_id: &str) -> Result<(), BrowserAutomationError> {
408    automation::browser_clear_cookies(tab_id).await
409}
410
411pub async fn clear_site_data(
412    tab_id: &str,
413    options: lingxia_webview::ClearSiteDataOptions,
414) -> Result<lingxia_webview::ClearSiteDataResult, BrowserAutomationError> {
415    automation::browser_clear_site_data(tab_id, options).await
416}
417
418pub async fn start_network_capture(tab_id: &str) -> Result<(), BrowserAutomationError> {
419    automation::browser_start_network_capture(tab_id).await
420}
421
422pub async fn stop_network_capture(tab_id: &str) -> Result<(), BrowserAutomationError> {
423    automation::browser_stop_network_capture(tab_id).await
424}
425
426pub async fn network_entries(
427    tab_id: &str,
428) -> Result<NetworkCaptureSnapshot, BrowserAutomationError> {
429    automation::browser_network_entries(tab_id).await
430}
431
432pub async fn clear_network_capture(tab_id: &str) -> Result<(), BrowserAutomationError> {
433    automation::browser_clear_network_capture(tab_id).await
434}
435
436pub async fn query(
437    tab_id: &str,
438    selector: &str,
439) -> Result<BrowserElementInfo, BrowserAutomationError> {
440    automation::browser_query(tab_id, selector).await
441}
442
443pub async fn query_with_max_text(
444    tab_id: &str,
445    selector: &str,
446    max_text_chars: Option<usize>,
447) -> Result<BrowserElementInfo, BrowserAutomationError> {
448    automation::browser_query_with_max_text(tab_id, selector, max_text_chars).await
449}
450
451pub async fn wait(
452    tab_id: &str,
453    condition: BrowserWaitCondition,
454    timeout: Duration,
455) -> Result<BrowserWaitResult, BrowserAutomationError> {
456    automation::browser_wait(tab_id, condition, timeout).await
457}
458
459pub async fn wait_for_url(
460    tab_id: &str,
461    url: &str,
462    timeout: Duration,
463) -> Result<BrowserWaitResult, BrowserAutomationError> {
464    automation::browser_wait_for_url(tab_id, url, timeout).await
465}
466
467pub async fn wait_for_url_contains(
468    tab_id: &str,
469    text: &str,
470    timeout: Duration,
471) -> Result<BrowserWaitResult, BrowserAutomationError> {
472    automation::browser_wait_for_url_contains(tab_id, text, timeout).await
473}
474
475pub async fn wait_for_navigation(
476    tab_id: &str,
477    timeout: Duration,
478    wait_until_complete: bool,
479) -> Result<BrowserWaitResult, BrowserAutomationError> {
480    automation::browser_wait_for_navigation(tab_id, timeout, wait_until_complete).await
481}
482
483pub async fn click(tab_id: &str, selector: &str) -> Result<(), BrowserAutomationError> {
484    automation::browser_click(tab_id, selector).await
485}
486
487pub async fn type_text(
488    tab_id: &str,
489    selector: &str,
490    text: &str,
491) -> Result<(), BrowserAutomationError> {
492    automation::browser_type_text(tab_id, selector, text).await
493}
494
495pub async fn fill(tab_id: &str, selector: &str, text: &str) -> Result<(), BrowserAutomationError> {
496    automation::browser_fill(tab_id, selector, text).await
497}
498
499pub async fn press(tab_id: &str, key: &str) -> Result<(), BrowserAutomationError> {
500    automation::browser_press(tab_id, key).await
501}
502
503pub async fn scroll(tab_id: &str, dx: f64, dy: f64) -> Result<(), BrowserAutomationError> {
504    automation::browser_scroll(tab_id, dx, dy).await
505}
506
507pub async fn scroll_to(tab_id: &str, selector: &str) -> Result<(), BrowserAutomationError> {
508    automation::browser_scroll_to(tab_id, selector).await
509}
510
511pub fn tab_path(tab_id: &str) -> String {
512    tabs::browser_tab_path_for_id(tab_id)
513}
514
515pub fn update_tab(tab_id: &str, current_url: Option<&str>, title: Option<&str>) -> bool {
516    tabs::browser_update_tab_info(tab_id, current_url, title)
517}
518
519pub fn start_download(
520    tab_id: &str,
521    url: &str,
522    user_agent: Option<&str>,
523    suggested_filename: Option<&str>,
524    source_page_url: Option<&str>,
525    cookie: Option<&str>,
526) -> Result<(), LxAppError> {
527    downloads::start_native_browser_download(
528        tab_id,
529        url,
530        user_agent,
531        suggested_filename,
532        source_page_url,
533        cookie,
534    )
535}
536
537#[doc(hidden)]
538pub fn register_bundled_app() {
539    tabs::register_builtin_browser_host();
540}
541
542#[doc(hidden)]
543pub fn warmup() {
544    if let Err(err) = internal_pages::warmup_builtin_browser_runtime() {
545        lxapp::warn!("[InternalBrowser] warmup failed: {}", err);
546    }
547}
548
549#[cfg(test)]
550mod authority_tests {
551    use super::*;
552
553    #[test]
554    fn app_owned_navigation_cannot_open_internal_control_routes() {
555        let error = open_for_app("app.example", 7, "lingxia://downloads", Some("downloads"))
556            .expect_err("ordinary app navigation must reject internal routes");
557        assert!(matches!(error, LxAppError::UnsupportedOperation(_)));
558        assert!(
559            error
560                .to_string()
561                .contains("sealed native browser authority")
562        );
563    }
564}
565
566pub use tabs::{ClosedBrowserTab, recently_closed, reopen_closed};