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";
36pub const CONTROL_PROTOCOL_VERSION: u32 = 3;
38pub 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#[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#[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
192pub 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
212pub 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
236pub fn tab_is_aside(tab_id: &str) -> bool {
238 tabs::is_aside_tab(tab_id)
239}
240
241pub 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
262pub 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
267pub fn discard(tab_id: &str) -> Result<(), LxAppError> {
269 tabs::discard_browser_tab(tab_id)
270}
271
272pub fn reactivate(tab_id: &str) -> Result<(), LxAppError> {
274 tabs::reactivate_browser_tab(tab_id)
275}
276
277pub fn mark_active(tab_id: &str) {
279 tabs::mark_browser_tab_active(tab_id)
280}
281
282pub 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
295pub 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
305pub fn set_tabs_changed_handler(handler: Arc<dyn Fn() + Send + Sync>) {
314 tabs::set_tabs_changed_handler(handler);
315}
316
317pub fn set_tab_present_handler(handler: Arc<dyn Fn(&str) + Send + Sync>) {
323 tabs::set_tab_present_handler(handler);
324}
325
326pub fn present(tab_id: &str) -> Result<BrowserTabInfo, BrowserAutomationError> {
328 tabs::browser_present_tab(tab_id)
329}
330
331pub 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
362pub fn configured_user_agent() -> Option<String> {
365 automation::browser_configured_user_agent()
366}
367
368pub 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};