From f8849308b53b827d573267f98eb6eb22b716e199 Mon Sep 17 00:00:00 2001 From: F0RLE Date: Mon, 23 Feb 2026 19:11:07 +0300 Subject: [PATCH 01/26] feat: add permission handler API - implement cross-platform permission handler API, fix combined camera and microphone logic on macOS, update documentation to clarify NFC and Bluetooth support, and simplify permission_handler example using permission.site --- .changes/permission-handler.md | 12 + examples/permission_handler.rs | 47 ++++ src/android/binding.rs | 62 ++++- src/android/kotlin/RustWebChromeClient.kt | 33 ++- src/android/mod.rs | 5 +- src/custom_protocol_workaround.rs | 2 +- src/lib.rs | 236 +++++++++++++++--- src/webkitgtk/mod.rs | 56 ++++- src/webview2/mod.rs | 56 ++++- .../class/wry_web_view_ui_delegate.rs | 46 +++- src/wkwebview/mod.rs | 7 +- 11 files changed, 503 insertions(+), 59 deletions(-) create mode 100644 .changes/permission-handler.md create mode 100644 examples/permission_handler.rs diff --git a/.changes/permission-handler.md b/.changes/permission-handler.md new file mode 100644 index 000000000..086a6f794 --- /dev/null +++ b/.changes/permission-handler.md @@ -0,0 +1,12 @@ +--- +"wry": minor +--- + +Add an expanded permission handling API for WebView2, WKWebView, WebKitGTK, and Android. +This includes: +- `PermissionKind` expansion: `DisplayCapture`, `Midi`, `Sensors`, `MediaKeySystemAccess`, `LocalFonts`, `WindowManagement`, `PointerLock`, `AutomaticDownloads`, `FileSystemAccess`, `Autoplay`. +- Support for `PermissionResponse::Prompt` to trigger native system dialogs. +- Android support via JNI bridge (`onPermissionRequest` in `RustWebChromeClient`). +- macOS: Split camera/microphone requests; `CameraAndMicrophone` resolved from individual responses. +- Linux: `DisplayCapture` detection for WebKitGTK < 2.42 (getDisplayMedia fix). +- Windows: Full coverage of all 12 `COREWEBVIEW2_PERMISSION_KIND` values. diff --git a/examples/permission_handler.rs b/examples/permission_handler.rs new file mode 100644 index 000000000..78f68c2ce --- /dev/null +++ b/examples/permission_handler.rs @@ -0,0 +1,47 @@ +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Example demonstrating the permission handler API. +//! +//! Run: cargo run --example permission_handler +//! Then click the buttons and watch the terminal output. + +fn main() -> wry::Result<()> { + use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, + }; + use wry::{PermissionKind, PermissionResponse, WebViewBuilder}; + + let event_loop = EventLoop::new(); + let window = WindowBuilder::new() + .with_title("Permission Handler Example") + .with_inner_size(tao::dpi::LogicalSize::new(800, 600)) + .build(&event_loop) + .unwrap(); + + let _webview = WebViewBuilder::new() + .with_url("https://permission.site/") + .with_permission_handler(|kind| { + let response = match kind { + PermissionKind::Geolocation => PermissionResponse::Prompt, + _ => PermissionResponse::Allow, + }; + println!("[permission] {kind} → {response}"); + response + }) + .build(&window)?; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + if let Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } = event + { + *control_flow = ControlFlow::Exit; + } + }); +} diff --git a/src/android/binding.rs b/src/android/binding.rs index 8e7156834..47a45f60a 100644 --- a/src/android/binding.rs +++ b/src/android/binding.rs @@ -16,11 +16,11 @@ pub use jni::{ pub use ndk; use super::{ - ASSET_LOADER_DOMAIN, EVAL_CALLBACKS, IPC, ON_LOAD_HANDLER, REQUEST_HANDLER, TITLE_CHANGE_HANDLER, - URL_LOADING_OVERRIDE, WITH_ASSET_LOADER, + ASSET_LOADER_DOMAIN, EVAL_CALLBACKS, IPC, ON_LOAD_HANDLER, PERMISSION_HANDLER, REQUEST_HANDLER, + TITLE_CHANGE_HANDLER, URL_LOADING_OVERRIDE, WITH_ASSET_LOADER, }; -use crate::PageLoadEvent; +use crate::{PageLoadEvent, PermissionKind, PermissionResponse}; #[macro_export] macro_rules! android_binding { @@ -97,6 +97,14 @@ macro_rules! android_binding { handleReceivedTitle, [JObject, JString], ); + android_fn!( + $domain, + $package, + RustWebChromeClient, + onPermissionRequestNative, + [jni::objects::JObjectArray], + jint + ); }}; } @@ -413,3 +421,51 @@ pub unsafe fn onPageLoaded(mut env: JNIEnv, _: JClass, url: JString) { } } } + +pub unsafe fn onPermissionRequestNative( + mut env: JNIEnv, + _: JClass, + resources: jni::objects::JObjectArray, +) -> jint { + let mut allowed = false; + let mut denied = false; + let mut prompt = false; + + if let Ok(size) = env.get_array_length(&resources) { + for i in 0..size { + if let Ok(resource) = env.get_object_array_element(&resources, i) { + if let Ok(resource_str) = env.get_string(&resource.into()) { + let resource_str = resource_str.to_string_lossy(); + + let kind = match resource_str.as_ref() { + "android.webkit.resource.AUDIO_CAPTURE" => PermissionKind::Microphone, + "android.webkit.resource.VIDEO_CAPTURE" => PermissionKind::Camera, + "android.webkit.resource.PROTECTED_MEDIA_ID" => PermissionKind::MediaKeySystemAccess, + "android.webkit.resource.MIDI_SYSEX" => PermissionKind::Midi, + _ => PermissionKind::Other, + }; + + if let Some(handler) = &*PERMISSION_HANDLER.lock().unwrap() { + match (handler.handler)(kind) { + PermissionResponse::Allow => allowed = true, + PermissionResponse::Deny => denied = true, + PermissionResponse::Prompt => prompt = true, + PermissionResponse::Default => {} + } + } + } + } + } + } + + // Consolidated decision logic + if denied { + 1 // Deny + } else if allowed { + 0 // Allow + } else if prompt { + 3 // Prompt + } else { + 2 // Default + } +} diff --git a/src/android/kotlin/RustWebChromeClient.kt b/src/android/kotlin/RustWebChromeClient.kt index b89192602..02176e26a 100644 --- a/src/android/kotlin/RustWebChromeClient.kt +++ b/src/android/kotlin/RustWebChromeClient.kt @@ -92,6 +92,24 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { } override fun onPermissionRequest(request: PermissionRequest) { + val response = onPermissionRequestNative(request.resources) + when (response) { + 0 -> { // Allow + request.grant(request.resources) + return + } + 1 -> { // Deny + request.deny() + return + } + 2 -> { // Default + // Continue with default logic + } + 3 -> { // Prompt + // Continue with default logic (which prompts) + } + } + val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M val permissionList: MutableList = ArrayList() if (listOf(*request.resources).contains("android.webkit.resource.VIDEO_CAPTURE")) { @@ -118,6 +136,8 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { } } + private external fun onPermissionRequestNative(resources: Array): Int + /** * Show the browser alert modal * @param view @@ -482,12 +502,15 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { return File.createTempFile(imageFileName, ".jpg", storageDir) } - override fun onReceivedTitle( - view: WebView, - title: String - ) { - handleReceivedTitle(view, title) + override fun onPermissionRequest(request: PermissionRequest) { + val result = onPermissionRequestNative(request.resources) + when (result) { + 0 -> request.grant(request.resources) + 1 -> request.deny() + else -> super.onPermissionRequest(request) + } } + private external fun onPermissionRequestNative(resources: Array): Int private external fun handleReceivedTitle(webview: WebView, title: String) } diff --git a/src/android/mod.rs b/src/android/mod.rs index 5c7905da4..b78d5c54a 100644 --- a/src/android/mod.rs +++ b/src/android/mod.rs @@ -3,7 +3,9 @@ // SPDX-License-Identifier: MIT use super::{PageLoadEvent, WebViewAttributes, RGBA}; -use crate::{custom_protocol_workaround, RequestAsyncResponder, Result}; +use crate::{ + custom_protocol_workaround, PermissionKind, PermissionResponse, RequestAsyncResponder, Result, +}; use base64::{engine::general_purpose, Engine}; use crossbeam_channel::*; use html5ever::{interface::QualName, namespace_url, ns, tendril::TendrilSink, LocalName}; @@ -81,6 +83,7 @@ define_static_handlers! { TITLE_CHANGE_HANDLER = UnsafeTitleHandler { handler: Box }; URL_LOADING_OVERRIDE = UnsafeUrlLoadingOverride { handler: Box bool> }; ON_LOAD_HANDLER = UnsafeOnPageLoadHandler { handler: Box }; + PERMISSION_HANDLER = UnsafePermissionHandler { handler: Box PermissionResponse> }; } pub static WITH_ASSET_LOADER: StaticValue> = StaticValue(Mutex::new(None)); diff --git a/src/custom_protocol_workaround.rs b/src/custom_protocol_workaround.rs index 12fa7e41f..122b1341d 100644 --- a/src/custom_protocol_workaround.rs +++ b/src/custom_protocol_workaround.rs @@ -1,5 +1,5 @@ //! - WebView2 supports non-standard protocols only on Windows 10+, so we have to use a workaround. -//! See https://github.com/MicrosoftEdge/WebView2Feedback/issues/73 +//! See //! - On Android, there's no API for registering custom protocols, so this workaround is also used. //! //! The process looks like this: diff --git a/src/lib.rs b/src/lib.rs index 1db067c1f..68533eb51 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,7 +51,7 @@ //! event_loop.run_app(&mut app).unwrap(); //! ``` //! -//! If you also want to support Wayland too, then we recommend you use [`WebViewBuilderExtUnix::new_gtk`] on Linux. +//! If you also want to support Wayland too, then we recommend you use `WebViewBuilderExtUnix::new_gtk` on Linux. //! See the following example using [`tao`]: //! //! ```no_run @@ -111,7 +111,7 @@ //! ``` //! //! If you want to support X11 and Wayland at the same time, we recommend using -//! [`WebViewExtUnix::new_gtk`] or [`WebViewBuilderExtUnix::new_gtk`] with [`gtk::Fixed`]. +//! `WebViewExtUnix::new_gtk` or `WebViewBuilderExtUnix::new_gtk` with `gtk::Fixed`. //! //! ```no_run //! # use wry::{WebViewBuilder, raw_window_handle, Rect, dpi::*}; @@ -151,7 +151,7 @@ //! //! [WebKitGTK](https://webkitgtk.org/) is used to provide webviews on Linux which requires GTK, //! so if the windowing library doesn't support GTK (as in [`winit`]) -//! you'll need to call [`gtk::init`] before creating the webview and then call [`gtk::main_iteration_do`] alongside +//! you'll need to call `gtk::init` before creating the webview and then call `gtk::main_iteration_do` alongside //! your windowing library event loop. //! //! ```no_run @@ -464,7 +464,7 @@ pub enum NewWindowResponse { /// /// ## Platform-specific: /// - /// **Linux**: The webview must be related to the caller webview. See [`WebViewBuilderExtUnix::with_related_view`]. + /// **Linux**: The webview must be related to the caller webview. See `WebViewBuilderExtUnix::with_related_view`. /// **Windows**: The webview must use the same environment as the caller webview. See [`WebViewBuilderExtWindows::with_environment`]. /// **macOS**: The webview must use the same configuration as the caller webview. See [`WebViewBuilderExtMacos::with_webview_configuration`]. #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -535,6 +535,104 @@ pub struct NewWindowFeatures { pub opener: NewWindowOpener, } +/// Permission types that can be requested by the webview. +/// +/// See [`WebViewBuilder::with_permission_handler`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum PermissionKind { + /// Microphone access permission. + Microphone, + /// Camera access permission. + Camera, + /// Geolocation access permission. + Geolocation, + /// Notifications permission. + Notifications, + /// Clipboard read permission. + ClipboardRead, + /// Display capture permission (for getDisplayMedia). + DisplayCapture, + /// Midi access permission. + Midi, + /// Sensors (accelerometer, gyroscope, etc.) access permission. + Sensors, + /// Media key system access permission. + MediaKeySystemAccess, + /// Local fonts access permission. + LocalFonts, + /// Window management permission. + WindowManagement, + /// Pointer lock permission. + PointerLock, + /// Automatic downloads permission (multiple downloads without user interaction). + AutomaticDownloads, + /// File system access permission (read/write via File System Access API). + /// + /// ## Platform-specific + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_FILE_READ_WRITE`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + FileSystemAccess, + /// Media autoplay permission. + /// + /// ## Platform-specific + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_AUTOPLAY`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + Autoplay, + /// Other unrecognized permission type. + Other, +} + +impl std::fmt::Display for PermissionKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Microphone => write!(f, "microphone"), + Self::Camera => write!(f, "camera"), + Self::Geolocation => write!(f, "geolocation"), + Self::Notifications => write!(f, "notifications"), + Self::ClipboardRead => write!(f, "clipboard-read"), + Self::DisplayCapture => write!(f, "display-capture"), + Self::Midi => write!(f, "midi"), + Self::Sensors => write!(f, "sensors"), + Self::MediaKeySystemAccess => write!(f, "media-key-system-access"), + Self::LocalFonts => write!(f, "local-fonts"), + Self::WindowManagement => write!(f, "window-management"), + Self::PointerLock => write!(f, "pointer-lock"), + Self::AutomaticDownloads => write!(f, "automatic-downloads"), + Self::FileSystemAccess => write!(f, "file-system-access"), + Self::Autoplay => write!(f, "autoplay"), + Self::Other => write!(f, "other"), + } + } +} + +/// Response for permission requests. +/// +/// See [`WebViewBuilder::with_permission_handler`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum PermissionResponse { + /// Grant the permission. + Allow, + /// Deny the permission. + Deny, + /// Use default behavior (show system prompt). + #[default] + Default, + /// Explicitly prompt the user (system dialog). + Prompt, +} + +impl std::fmt::Display for PermissionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Allow => write!(f, "allow"), + Self::Deny => write!(f, "deny"), + Self::Default => write!(f, "default"), + Self::Prompt => write!(f, "prompt"), + } + } +} + /// An id for a webview pub type WebViewId<'a> = &'a str; @@ -735,7 +833,7 @@ pub struct WebViewAttributes<'a> { /// ## Platform-specific: /// /// - Windows: Setting to `false` does nothing on WebView2 Runtime version before 92.0.902.0, - /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10902-prerelease + /// see /// /// - **Android / iOS:** Unsupported. pub back_forward_navigation_gestures: bool, @@ -749,7 +847,7 @@ pub struct WebViewAttributes<'a> { /// ## Platform-specific: /// /// - **Windows**: Requires WebView2 Runtime version 101.0.1210.39 or higher, does nothing on older versions, - /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10121039 + /// see /// - **Android:** Unsupported yet. /// - **macOS / iOS**: Uses the nonPersistent DataStore. pub incognito: bool, @@ -774,8 +872,8 @@ pub struct WebViewAttributes<'a> { pub focused: bool, /// The webview bounds. Defaults to `x: 0, y: 0, width: 200, height: 200`. - /// This is only effective if the webview was created by [`WebView::new_as_child`] or [`WebViewBuilder::new_as_child`] - /// or on Linux, if was created by [`WebViewExtUnix::new_gtk`] or [`WebViewBuilderExtUnix::new_gtk`] with [`gtk::Fixed`]. + /// This is only effective if the webview was created by [`WebView::new_as_child`] or `WebViewBuilder::new_as_child` + /// or on Linux, if was created by `WebViewExtUnix::new_gtk` or `WebViewBuilderExtUnix::new_gtk` with `gtk::Fixed`. pub bounds: Option, /// Whether background throttling should be disabled. @@ -790,12 +888,43 @@ pub struct WebViewAttributes<'a> { /// - **iOS**: Supported since version 17.0+. /// - **macOS**: Supported since version 14.0+. /// - /// see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578 + /// see pub background_throttling: Option, /// Whether JavaScript should be disabled. pub javascript_disabled: bool, + /// A handler to intercept permission requests from the webview. + /// + /// The handler receives the [`PermissionKind`] and should return + /// the desired [`PermissionResponse`]. + /// + /// > [!NOTE] + /// > This handler only triggers for new permission requests. If the user has already + /// > allowed or denied a permission persistently within the webview, the browser + /// > will use the saved preference instead of calling this handler. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Fully supported via WebView2's PermissionRequested event. + /// - **macOS / iOS**: Fully supported via WKUIDelegate's requestMediaCapturePermission. + /// - **Linux**: Fully supported via WebKitGTK's permission-request signal. + /// - **Android**: Supported via JNI bridge with some limitations (WIP). + /// + /// ## Example + /// + /// ```no_run + /// # use wry::{WebViewBuilder, PermissionKind, PermissionResponse}; + /// let webview = WebViewBuilder::new() + /// .with_permission_handler(|kind| { + /// match kind { + /// PermissionKind::Microphone => PermissionResponse::Allow, + /// PermissionKind::Camera => PermissionResponse::Allow, + /// _ => PermissionResponse::Default, + /// } + /// }); + /// ``` + pub permission_handler: Option PermissionResponse + Send + Sync>>, /// Controls the WebView's browser-level general autofill behavior. /// /// **This option does not disable password or credit card autofill.** @@ -855,6 +984,7 @@ impl Default for WebViewAttributes<'_> { }), background_throttling: None, javascript_disabled: false, + permission_handler: None, general_autofill_enabled: true, } } @@ -1044,7 +1174,7 @@ impl<'a> WebViewBuilder<'a> { /// # Reading assets on mobile /// /// - Android: For loading content from the `assets` folder (which is copied to the Andorid apk) please - /// use the function [`with_asset_loader`] from [`WebViewBuilderExtAndroid`] instead. + /// use the function `with_asset_loader` from `WebViewBuilderExtAndroid` instead. /// This function on Android can only be used to serve assets you can embed in the binary or are /// elsewhere in Android (provided the app has appropriate access), but not from the `assets` /// folder which lives within the apk. For the cases where this can be used, it works the same as in macOS and Linux. @@ -1237,7 +1367,7 @@ impl<'a> WebViewBuilder<'a> { /// ## Platform-specific /// /// - Windows: Requires WebView2 Runtime version 86.0.616.0 or higher, does nothing on older versions, - /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10790-prerelease + /// see pub fn with_user_agent(mut self, user_agent: impl Into) -> Self { self.attrs.user_agent = Some(user_agent.into()); self @@ -1264,7 +1394,7 @@ impl<'a> WebViewBuilder<'a> { /// ## Platform-specific /// /// - Windows: Setting to `false` can't disable pinch zoom on WebView2 Runtime version before 91.0.865.0, - /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10865-prerelease + /// see /// /// - **macOS / Linux / Android / iOS**: Unsupported pub fn with_hotkeys_zoom(mut self, zoom: bool) -> Self { @@ -1281,6 +1411,44 @@ impl<'a> WebViewBuilder<'a> { self } + /// Set a handler to intercept permission requests from the webview. + /// + /// The handler receives the [`PermissionKind`] and should return + /// the desired [`PermissionResponse`]. + /// + /// > [!NOTE] + /// > This handler only triggers for new permission requests. If the user has already + /// > allowed or denied a permission persistently within the webview, the browser + /// > will use the saved preference instead of calling this handler. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Fully supported via WebView2's PermissionRequested event. + /// - **macOS / iOS**: Fully supported via WKUIDelegate's requestMediaCapturePermission. + /// - **Linux**: Fully supported via WebKitGTK's permission-request signal. + /// - **Android**: Supported via JNI bridge with some limitations (WIP). + /// + /// ## Example + /// + /// ```no_run + /// # use wry::{WebViewBuilder, PermissionKind, PermissionResponse}; + /// let webview = WebViewBuilder::new() + /// .with_permission_handler(|kind| { + /// match kind { + /// PermissionKind::Microphone => PermissionResponse::Allow, + /// PermissionKind::Camera => PermissionResponse::Allow, + /// _ => PermissionResponse::Default, + /// } + /// }); + /// ``` + pub fn with_permission_handler(mut self, handler: F) -> Self + where + F: Fn(PermissionKind) -> PermissionResponse + Send + Sync + 'static, + { + self.attrs.permission_handler = Some(Box::new(handler)); + self + } + /// Set a download started handler to manage incoming downloads. /// /// The closure takes two parameters, the first is a `String` representing the url being downloaded from and and the @@ -1371,7 +1539,7 @@ impl<'a> WebViewBuilder<'a> { /// ## Platform-specific: /// /// - Windows: Requires WebView2 Runtime version 101.0.1210.39 or higher, does nothing on older versions, - /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10121039 + /// see /// - **Android:** Unsupported yet. pub fn with_incognito(mut self, incognito: bool) -> Self { self.attrs.incognito = incognito; @@ -1408,7 +1576,7 @@ impl<'a> WebViewBuilder<'a> { } /// Specify the webview position relative to its parent if it will be created as a child - /// or if created using [`WebViewBuilderExtUnix::new_gtk`] with [`gtk::Fixed`]. + /// or if created using `WebViewBuilderExtUnix::new_gtk` with `gtk::Fixed`. /// /// Defaults to `x: 0, y: 0, width: 200, height: 200`. pub fn with_bounds(mut self, bounds: Rect) -> Self { @@ -1428,7 +1596,7 @@ impl<'a> WebViewBuilder<'a> { /// - **iOS**: Supported since version 17.0+. /// - **macOS**: Supported since version 14.0+. /// - /// see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578 + /// see pub fn with_background_throttling(mut self, policy: BackgroundThrottlingPolicy) -> Self { self.attrs.background_throttling = Some(policy); self @@ -1465,10 +1633,10 @@ impl<'a> WebViewBuilder<'a> { /// /// # Platform-specific: /// - /// - **Linux**: Only X11 is supported, if you want to support Wayland too, use [`WebViewBuilderExtUnix::new_gtk`]. + /// - **Linux**: Only X11 is supported, if you want to support Wayland too, use `WebViewBuilderExtUnix::new_gtk`. /// /// Although this methods only needs an X11 window handle, we use webkit2gtk, so you still need to initialize gtk - /// by callling [`gtk::init`] and advance its loop alongside your event loop using [`gtk::main_iteration_do`]. + /// by callling `gtk::init` and advance its loop alongside your event loop using `gtk::main_iteration_do`. /// Checkout the [Platform Considerations](https://docs.rs/wry/latest/wry/#platform-considerations) section in the crate root documentation. /// - **Windows**: The webview will auto-resize when the passed handle is resized. /// - **Linux (X11)**: Unlike macOS and Windows, the webview will not auto-resize and you'll need to call [`WebView::set_bounds`] manually. @@ -1476,7 +1644,7 @@ impl<'a> WebViewBuilder<'a> { /// # Panics: /// /// - Panics if the provided handle was not supported or invalid. - /// - Panics on Linux, if [`gtk::init`] was not called in this thread. + /// - Panics on Linux, if `gtk::init` was not called in this thread. pub fn build(self, window: &'a W) -> Result { self.error?; @@ -1494,17 +1662,17 @@ impl<'a> WebViewBuilder<'a> { /// is supported. This method won't work on Wayland. /// /// Although this methods only needs an X11 window handle, you use webkit2gtk, so you still need to initialize gtk - /// by callling [`gtk::init`] and advance its loop alongside your event loop using [`gtk::main_iteration_do`]. + /// by callling `gtk::init` and advance its loop alongside your event loop using `gtk::main_iteration_do`. /// Checkout the [Platform Considerations](https://docs.rs/wry/latest/wry/#platform-considerations) section in the crate root documentation. /// /// If you want to support child webviews on X11 and Wayland at the same time, - /// we recommend using [`WebViewBuilderExtUnix::new_gtk`] with [`gtk::Fixed`]. + /// we recommend using `WebViewBuilderExtUnix::new_gtk` with `gtk::Fixed`. /// - **Android/iOS:** Unsupported. /// /// # Panics: /// /// - Panics if the provided handle was not support or invalid. - /// - Panics on Linux, if [`gtk::init`] was not called in this thread. + /// - Panics on Linux, if `gtk::init` was not called in this thread. pub fn build_as_child(self, window: &'a W) -> Result { self.error?; @@ -1731,7 +1899,7 @@ pub trait WebViewBuilderExtWindows { /// The default value is `true`. See the following link to know more details. /// /// Setting to `false` does nothing on WebView2 Runtime version before 92.0.902.0, - /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10824-prerelease + /// see /// /// fn with_browser_accelerator_keys(self, enabled: bool) -> Self; @@ -1749,7 +1917,7 @@ pub trait WebViewBuilderExtWindows { /// Defaults to [`Theme::Auto`] which will follow the OS defaults. /// /// Requires WebView2 Runtime version 101.0.1210.39 or higher, does nothing on older versions, - /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10121039 + /// see fn with_theme(self, theme: Theme) -> Self; /// Determines whether the custom protocols should use `https://.path/to/page` instead of the default `http://.path/to/page`. @@ -1763,10 +1931,10 @@ pub trait WebViewBuilderExtWindows { /// Specifies the native scrollbar style to use with webview2. /// CSS styles that modify the scrollbar are applied on top of the native appearance configured here. /// - /// Defaults to [`ScrollbarStyle::Default`] which is the browser default used by Microsoft Edge. + /// Defaults to `ScrollbarStyle::Default` which is the browser default used by Microsoft Edge. /// /// Requires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions, - /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541 + /// see fn with_scroll_bar_style(self, style: ScrollBarStyle) -> Self; /// Determines whether the ability to install and enable extensions is enabled. @@ -1774,7 +1942,7 @@ pub trait WebViewBuilderExtWindows { /// By default, extensions are disabled. /// /// Requires WebView2 Runtime version 1.0.2210.55 or higher, does nothing on older versions, - /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10221055 + /// see fn with_browser_extensions_enabled(self, enabled: bool) -> Self; /// Set the path from which to load extensions from. Extensions stored in this path should be unpacked. @@ -1995,10 +2163,10 @@ impl WebView { /// /// # Platform-specific: /// - /// - **Linux**: Only X11 is supported, if you want to support Wayland too, use [`WebViewExtUnix::new_gtk`]. + /// - **Linux**: Only X11 is supported, if you want to support Wayland too, use `WebViewExtUnix::new_gtk`. /// /// Although this methods only needs an X11 window handle, you use webkit2gtk, so you still need to initialize gtk - /// by callling [`gtk::init`] and advance its loop alongside your event loop using [`gtk::main_iteration_do`]. + /// by callling `gtk::init` and advance its loop alongside your event loop using `gtk::main_iteration_do`. /// Checkout the [Platform Considerations](https://docs.rs/wry/latest/wry/#platform-considerations) section in the crate root documentation. /// - **macOS / Windows**: The webview will auto-resize when the passed handle is resized. /// - **Linux (X11)**: Unlike macOS and Windows, the webview will not auto-resize and you'll need to call [`WebView::set_bounds`] manually. @@ -2006,7 +2174,7 @@ impl WebView { /// # Panics: /// /// - Panics if the provided handle was not supported or invalid. - /// - Panics on Linux, if [`gtk::init`] was not called in this thread. + /// - Panics on Linux, if `gtk::init` was not called in this thread. pub fn new(window: &impl HasWindowHandle, attrs: WebViewAttributes) -> Result { WebViewBuilder::new_with_attributes(attrs).build(window) } @@ -2022,17 +2190,17 @@ impl WebView { /// is supported. This method won't work on Wayland. /// /// Although this methods only needs an X11 window handle, you use webkit2gtk, so you still need to initialize gtk - /// by callling [`gtk::init`] and advance its loop alongside your event loop using [`gtk::main_iteration_do`]. + /// by callling `gtk::init` and advance its loop alongside your event loop using `gtk::main_iteration_do`. /// Checkout the [Platform Considerations](https://docs.rs/wry/latest/wry/#platform-considerations) section in the crate root documentation. /// /// If you want to support child webviews on X11 and Wayland at the same time, - /// we recommend using [`WebViewBuilderExtUnix::new_gtk`] with [`gtk::Fixed`]. + /// we recommend using `WebViewBuilderExtUnix::new_gtk` with `gtk::Fixed`. /// - **Android/iOS:** Unsupported. /// /// # Panics: /// /// - Panics if the provided handle was not support or invalid. - /// - Panics on Linux, if [`gtk::init`] was not called in this thread. + /// - Panics on Linux, if `gtk::init` was not called in this thread. pub fn new_as_child(parent: &impl HasWindowHandle, attrs: WebViewAttributes) -> Result { WebViewBuilder::new_with_attributes(attrs).build_as_child(parent) } @@ -2193,7 +2361,7 @@ impl WebView { /// Set the webview bounds. /// /// This is only effective if the webview was created as a child - /// or created using [`WebViewBuilderExtUnix::new_gtk`] with [`gtk::Fixed`]. + /// or created using `WebViewBuilderExtUnix::new_gtk` with `gtk::Fixed`. pub fn set_bounds(&self, bounds: Rect) -> Result<()> { self.webview.set_bounds(bounds) } @@ -2284,7 +2452,7 @@ pub trait WebViewExtWindows { /// Changes the webview2 theme. /// /// Requires WebView2 Runtime version 101.0.1210.39 or higher, returns error on older versions, - /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10121039 + /// see fn set_theme(&self, theme: Theme) -> Result<()>; /// Sets the [memory usage target level][1]. diff --git a/src/webkitgtk/mod.rs b/src/webkitgtk/mod.rs index 6be022caf..18c996d5c 100644 --- a/src/webkitgtk/mod.rs +++ b/src/webkitgtk/mod.rs @@ -13,6 +13,7 @@ use gdkx11::{ }; #[cfg(feature = "x11")] use gtk::glib::{self, translate::FromGlibPtrFull}; +use gtk::glib::{Cast, IsA}; use gtk::{ gdk::{self}, gio::Cancellable, @@ -36,9 +37,10 @@ use std::{ use webkit2gtk::WebInspectorExt; use webkit2gtk::{ AutoplayPolicy, CookieManagerExt, InputMethodContextExt, LoadEvent, NavigationPolicyDecision, - NavigationPolicyDecisionExt, NetworkProxyMode, NetworkProxySettings, PolicyDecisionType, - PrintOperationExt, SettingsExt, URIRequest, URIRequestExt, UserContentInjectedFrames, - UserContentManager, UserContentManagerExt, UserScript, UserScriptInjectionTime, + NavigationPolicyDecisionExt, NetworkProxyMode, NetworkProxySettings, PermissionRequestExt, + PolicyDecisionType, PrintOperationExt, SettingsExt, URIRequest, URIRequestExt, + UserContentInjectedFrames, UserContentManager, UserContentManagerExt, UserMediaPermissionRequest, + UserMediaPermissionRequestExt, UserScript, UserScriptInjectionTime, WebContextExt as Webkit2gtkWeContextExt, WebView, WebViewExt, WebsiteDataManagerExt, WebsiteDataManagerExtManual, WebsitePolicies, }; @@ -53,7 +55,8 @@ pub use web_context::WebContextImpl; use crate::{ proxy::ProxyConfig, web_context::WebContext, Error, NewWindowFeatures, NewWindowOpener, - NewWindowResponse, PageLoadEvent, Rect, Result, WebViewAttributes, RGBA, + NewWindowResponse, PageLoadEvent, PermissionKind, PermissionResponse, Rect, Result, + WebViewAttributes, RGBA, }; use self::web_context::WebContextExt; @@ -573,6 +576,51 @@ impl InnerWebView { }); } + // Permission handler + if let Some(permission_handler) = attributes.permission_handler.take() { + webview.connect_permission_request(move |_webview, request| { + // Determine permission kind + let permission_kind = + if let Some(media_request) = request.downcast_ref::() { + if media_request.is_for_audio_device() { + PermissionKind::Microphone + } else if media_request.is_for_video_device() { + PermissionKind::Camera + } else { + // On WebKitGTK < 2.42, there's no is_for_display_device() + // but screen sharing requests come through UserMediaPermissionRequest + PermissionKind::DisplayCapture + } + } else if request.type_().name() == "WebKitGeolocationPermissionRequest" { + PermissionKind::Geolocation + } else if request.type_().name() == "WebKitNotificationPermissionRequest" { + PermissionKind::Notifications + } else if request.type_().name() == "WebKitPointerLockPermissionRequest" { + PermissionKind::PointerLock + } else { + PermissionKind::Other + }; + + // Call user's permission handler + let response = permission_handler(permission_kind); + + // Apply the response + match response { + PermissionResponse::Allow => { + request.allow(); + true // handled + } + PermissionResponse::Deny => { + request.deny(); + true // handled + } + PermissionResponse::Default | PermissionResponse::Prompt => { + false // not handled, let WebKitGTK show default prompt + } + } + }); + } + // Download handler if attributes.download_started_handler.is_some() || attributes.download_completed_handler.is_some() diff --git a/src/webview2/mod.rs b/src/webview2/mod.rs index 4ed6e9271..94ad94f0a 100644 --- a/src/webview2/mod.rs +++ b/src/webview2/mod.rs @@ -30,8 +30,8 @@ use self::drag_drop::DragDropController; use super::Theme; use crate::{ custom_protocol_workaround, proxy::ProxyConfig, Error, MemoryUsageLevel, NewWindowFeatures, - NewWindowOpener, NewWindowResponse, PageLoadEvent, Rect, RequestAsyncResponder, Result, - WebViewAttributes, RGBA, + NewWindowOpener, NewWindowResponse, PageLoadEvent, PermissionKind, PermissionResponse, Rect, + RequestAsyncResponder, Result, WebViewAttributes, RGBA, }; type EventRegistrationToken = i64; @@ -511,6 +511,58 @@ impl InnerWebView { } } + // Permission handler + if let Some(permission_handler) = attributes.permission_handler.take() { + unsafe { + webview.add_PermissionRequested( + &PermissionRequestedEventHandler::create(Box::new(move |_, args| { + let Some(args) = args else { return Ok(()) }; + + let mut kind = COREWEBVIEW2_PERMISSION_KIND::default(); + args.PermissionKind(&mut kind)?; + + // Convert WebView2 permission kind to our PermissionKind + let permission_kind = match kind { + COREWEBVIEW2_PERMISSION_KIND_MICROPHONE => PermissionKind::Microphone, + COREWEBVIEW2_PERMISSION_KIND_CAMERA => PermissionKind::Camera, + COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION => PermissionKind::Geolocation, + COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS => PermissionKind::Notifications, + COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ => PermissionKind::ClipboardRead, + COREWEBVIEW2_PERMISSION_KIND_LOCAL_FONTS => PermissionKind::LocalFonts, + COREWEBVIEW2_PERMISSION_KIND_OTHER_SENSORS => PermissionKind::Sensors, + COREWEBVIEW2_PERMISSION_KIND_MIDI_SYSTEM_EXCLUSIVE_MESSAGES => PermissionKind::Midi, + COREWEBVIEW2_PERMISSION_KIND_MULTIPLE_AUTOMATIC_DOWNLOADS => { + PermissionKind::AutomaticDownloads + } + COREWEBVIEW2_PERMISSION_KIND_FILE_READ_WRITE => PermissionKind::FileSystemAccess, + COREWEBVIEW2_PERMISSION_KIND_AUTOPLAY => PermissionKind::Autoplay, + COREWEBVIEW2_PERMISSION_KIND_WINDOW_MANAGEMENT => PermissionKind::WindowManagement, + _ => PermissionKind::Other, + }; + + // Call user's permission handler + let response = permission_handler(permission_kind); + + // Apply the response + match response { + PermissionResponse::Allow => { + args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW)?; + } + PermissionResponse::Deny => { + args.SetState(COREWEBVIEW2_PERMISSION_STATE_DENY)?; + } + PermissionResponse::Default | PermissionResponse::Prompt => { + // Do nothing, let WebView2 show default prompt + } + } + + Ok(()) + })), + &mut token, + )?; + } + } + // Navigation if let Some(mut url) = attributes.url { if let Some((protocol, _)) = url.split_once("://") { diff --git a/src/wkwebview/class/wry_web_view_ui_delegate.rs b/src/wkwebview/class/wry_web_view_ui_delegate.rs index 88f633313..034f98154 100644 --- a/src/wkwebview/class/wry_web_view_ui_delegate.rs +++ b/src/wkwebview/class/wry_web_view_ui_delegate.rs @@ -6,9 +6,9 @@ use std::{cell::RefCell, ptr::null_mut, rc::Rc}; use block2::Block; -#[cfg(target_os = "macos")] -use objc2::DefinedClass; -use objc2::{define_class, msg_send, rc::Retained, runtime::NSObject, MainThreadOnly}; +use objc2::{ + define_class, msg_send, rc::Retained, runtime::NSObject, DefinedClass, MainThreadOnly, +}; #[cfg(target_os = "macos")] use objc2_app_kit::{NSModalResponse, NSModalResponseOK, NSOpenPanel, NSWindowDelegate}; use objc2_foundation::{MainThreadMarker, NSObjectProtocol}; @@ -21,7 +21,7 @@ use objc2_web_kit::{ WKFrameInfo, WKMediaCaptureType, WKPermissionDecision, WKSecurityOrigin, WKUIDelegate, }; -use crate::{NewWindowFeatures, NewWindowResponse, WryWebView}; +use crate::{NewWindowFeatures, NewWindowResponse, PermissionKind, PermissionResponse, WryWebView}; #[cfg(target_os = "macos")] struct NewWindow { @@ -86,6 +86,7 @@ pub struct WryWebViewUIDelegateIvars { Option NewWindowResponse + Send + Sync>>, #[cfg(target_os = "macos")] new_windows: Rc>>, + permission_handler: Option PermissionResponse + Send + Sync>>, } define_class!( @@ -132,11 +133,40 @@ define_class!( _webview: &WryWebView, _origin: &WKSecurityOrigin, _frame: &WKFrameInfo, - _capture_type: WKMediaCaptureType, + capture_type: WKMediaCaptureType, decision_handler: &Block, ) { - //https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc - (*decision_handler).call((WKPermissionDecision::Grant,)); + // Call user's permission handler if set + let decision = if let Some(handler) = &self.ivars().permission_handler { + let translate_response = |res: PermissionResponse| match res { + PermissionResponse::Allow => WKPermissionDecision::Grant, + PermissionResponse::Deny => WKPermissionDecision::Deny, + PermissionResponse::Default => WKPermissionDecision::Prompt, + PermissionResponse::Prompt => WKPermissionDecision::Prompt, + }; + + match capture_type { + WKMediaCaptureType::Camera => translate_response(handler(PermissionKind::Camera)), + WKMediaCaptureType::Microphone => translate_response(handler(PermissionKind::Microphone)), + WKMediaCaptureType::CameraAndMicrophone => { + let mic_res = handler(PermissionKind::Microphone); + let cam_res = handler(PermissionKind::Camera); + + match (mic_res, cam_res) { + (PermissionResponse::Allow, PermissionResponse::Allow) => WKPermissionDecision::Grant, + (PermissionResponse::Deny, _) | (_, PermissionResponse::Deny) => { + WKPermissionDecision::Deny + } + _ => WKPermissionDecision::Prompt, + } + } + _ => translate_response(handler(PermissionKind::Other)), + } + } else { + WKPermissionDecision::Grant + }; + + (*decision_handler).call((decision,)); } #[cfg(target_os = "macos")] @@ -270,6 +300,7 @@ impl WryWebViewUIDelegate { new_window_req_handler: Option< Box NewWindowResponse + Send + Sync>, >, + permission_handler: Option PermissionResponse + Send + Sync>>, ) -> Retained { #[cfg(target_os = "ios")] let _new_window_req_handler = new_window_req_handler; @@ -281,6 +312,7 @@ impl WryWebViewUIDelegate { new_window_req_handler, #[cfg(target_os = "macos")] new_windows: Rc::new(RefCell::new(vec![])), + permission_handler, }); unsafe { msg_send![super(delegate), init] } } diff --git a/src/wkwebview/mod.rs b/src/wkwebview/mod.rs index 77f7a4719..bfab26785 100644 --- a/src/wkwebview/mod.rs +++ b/src/wkwebview/mod.rs @@ -577,8 +577,11 @@ impl InnerWebView { let proto_navigation_policy_delegate = ProtocolObject::from_ref(&*navigation_policy_delegate); webview.setNavigationDelegate(Some(proto_navigation_policy_delegate)); - let ui_delegate: Retained = - WryWebViewUIDelegate::new(mtm, attributes.new_window_req_handler); + let ui_delegate: Retained = WryWebViewUIDelegate::new( + mtm, + attributes.new_window_req_handler, + attributes.permission_handler, + ); let proto_ui_delegate = ProtocolObject::from_ref(&*ui_delegate); webview.setUIDelegate(Some(proto_ui_delegate)); From 33e6332f81bca3b77dabeb1d12d8dc1999bd2b06 Mon Sep 17 00:00:00 2001 From: F0RLE Date: Tue, 24 Feb 2026 15:30:07 +0300 Subject: [PATCH 02/26] fix(wkwebview): remove leftover PermissionObserver from UIDelegate signature --- src/custom_protocol_workaround.rs | 2 +- src/lib.rs | 68 +++++++++++++++---------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/custom_protocol_workaround.rs b/src/custom_protocol_workaround.rs index 122b1341d..12fa7e41f 100644 --- a/src/custom_protocol_workaround.rs +++ b/src/custom_protocol_workaround.rs @@ -1,5 +1,5 @@ //! - WebView2 supports non-standard protocols only on Windows 10+, so we have to use a workaround. -//! See +//! See https://github.com/MicrosoftEdge/WebView2Feedback/issues/73 //! - On Android, there's no API for registering custom protocols, so this workaround is also used. //! //! The process looks like this: diff --git a/src/lib.rs b/src/lib.rs index 68533eb51..5b5e54a50 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,7 +51,7 @@ //! event_loop.run_app(&mut app).unwrap(); //! ``` //! -//! If you also want to support Wayland too, then we recommend you use `WebViewBuilderExtUnix::new_gtk` on Linux. +//! If you also want to support Wayland too, then we recommend you use [`WebViewBuilderExtUnix::new_gtk`] on Linux. //! See the following example using [`tao`]: //! //! ```no_run @@ -111,7 +111,7 @@ //! ``` //! //! If you want to support X11 and Wayland at the same time, we recommend using -//! `WebViewExtUnix::new_gtk` or `WebViewBuilderExtUnix::new_gtk` with `gtk::Fixed`. +//! [`WebViewExtUnix::new_gtk`] or [`WebViewBuilderExtUnix::new_gtk`] with [`gtk::Fixed`]. //! //! ```no_run //! # use wry::{WebViewBuilder, raw_window_handle, Rect, dpi::*}; @@ -151,7 +151,7 @@ //! //! [WebKitGTK](https://webkitgtk.org/) is used to provide webviews on Linux which requires GTK, //! so if the windowing library doesn't support GTK (as in [`winit`]) -//! you'll need to call `gtk::init` before creating the webview and then call `gtk::main_iteration_do` alongside +//! you'll need to call [`gtk::init`] before creating the webview and then call [`gtk::main_iteration_do`] alongside //! your windowing library event loop. //! //! ```no_run @@ -464,7 +464,7 @@ pub enum NewWindowResponse { /// /// ## Platform-specific: /// - /// **Linux**: The webview must be related to the caller webview. See `WebViewBuilderExtUnix::with_related_view`. + /// **Linux**: The webview must be related to the caller webview. See [`WebViewBuilderExtUnix::with_related_view`]. /// **Windows**: The webview must use the same environment as the caller webview. See [`WebViewBuilderExtWindows::with_environment`]. /// **macOS**: The webview must use the same configuration as the caller webview. See [`WebViewBuilderExtMacos::with_webview_configuration`]. #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -833,7 +833,7 @@ pub struct WebViewAttributes<'a> { /// ## Platform-specific: /// /// - Windows: Setting to `false` does nothing on WebView2 Runtime version before 92.0.902.0, - /// see + /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10902-prerelease /// /// - **Android / iOS:** Unsupported. pub back_forward_navigation_gestures: bool, @@ -847,7 +847,7 @@ pub struct WebViewAttributes<'a> { /// ## Platform-specific: /// /// - **Windows**: Requires WebView2 Runtime version 101.0.1210.39 or higher, does nothing on older versions, - /// see + /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10121039 /// - **Android:** Unsupported yet. /// - **macOS / iOS**: Uses the nonPersistent DataStore. pub incognito: bool, @@ -872,8 +872,8 @@ pub struct WebViewAttributes<'a> { pub focused: bool, /// The webview bounds. Defaults to `x: 0, y: 0, width: 200, height: 200`. - /// This is only effective if the webview was created by [`WebView::new_as_child`] or `WebViewBuilder::new_as_child` - /// or on Linux, if was created by `WebViewExtUnix::new_gtk` or `WebViewBuilderExtUnix::new_gtk` with `gtk::Fixed`. + /// This is only effective if the webview was created by [`WebView::new_as_child`] or [`WebViewBuilder::new_as_child`] + /// or on Linux, if was created by [`WebViewExtUnix::new_gtk`] or [`WebViewBuilderExtUnix::new_gtk`] with [`gtk::Fixed`]. pub bounds: Option, /// Whether background throttling should be disabled. @@ -888,7 +888,7 @@ pub struct WebViewAttributes<'a> { /// - **iOS**: Supported since version 17.0+. /// - **macOS**: Supported since version 14.0+. /// - /// see + /// see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578 pub background_throttling: Option, /// Whether JavaScript should be disabled. @@ -1174,7 +1174,7 @@ impl<'a> WebViewBuilder<'a> { /// # Reading assets on mobile /// /// - Android: For loading content from the `assets` folder (which is copied to the Andorid apk) please - /// use the function `with_asset_loader` from `WebViewBuilderExtAndroid` instead. + /// use the function [`with_asset_loader`] from [`WebViewBuilderExtAndroid`] instead. /// This function on Android can only be used to serve assets you can embed in the binary or are /// elsewhere in Android (provided the app has appropriate access), but not from the `assets` /// folder which lives within the apk. For the cases where this can be used, it works the same as in macOS and Linux. @@ -1367,7 +1367,7 @@ impl<'a> WebViewBuilder<'a> { /// ## Platform-specific /// /// - Windows: Requires WebView2 Runtime version 86.0.616.0 or higher, does nothing on older versions, - /// see + /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10790-prerelease pub fn with_user_agent(mut self, user_agent: impl Into) -> Self { self.attrs.user_agent = Some(user_agent.into()); self @@ -1394,7 +1394,7 @@ impl<'a> WebViewBuilder<'a> { /// ## Platform-specific /// /// - Windows: Setting to `false` can't disable pinch zoom on WebView2 Runtime version before 91.0.865.0, - /// see + /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10865-prerelease /// /// - **macOS / Linux / Android / iOS**: Unsupported pub fn with_hotkeys_zoom(mut self, zoom: bool) -> Self { @@ -1539,7 +1539,7 @@ impl<'a> WebViewBuilder<'a> { /// ## Platform-specific: /// /// - Windows: Requires WebView2 Runtime version 101.0.1210.39 or higher, does nothing on older versions, - /// see + /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10121039 /// - **Android:** Unsupported yet. pub fn with_incognito(mut self, incognito: bool) -> Self { self.attrs.incognito = incognito; @@ -1576,7 +1576,7 @@ impl<'a> WebViewBuilder<'a> { } /// Specify the webview position relative to its parent if it will be created as a child - /// or if created using `WebViewBuilderExtUnix::new_gtk` with `gtk::Fixed`. + /// or if created using [`WebViewBuilderExtUnix::new_gtk`] with [`gtk::Fixed`]. /// /// Defaults to `x: 0, y: 0, width: 200, height: 200`. pub fn with_bounds(mut self, bounds: Rect) -> Self { @@ -1596,7 +1596,7 @@ impl<'a> WebViewBuilder<'a> { /// - **iOS**: Supported since version 17.0+. /// - **macOS**: Supported since version 14.0+. /// - /// see + /// see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578 pub fn with_background_throttling(mut self, policy: BackgroundThrottlingPolicy) -> Self { self.attrs.background_throttling = Some(policy); self @@ -1633,10 +1633,10 @@ impl<'a> WebViewBuilder<'a> { /// /// # Platform-specific: /// - /// - **Linux**: Only X11 is supported, if you want to support Wayland too, use `WebViewBuilderExtUnix::new_gtk`. + /// - **Linux**: Only X11 is supported, if you want to support Wayland too, use [`WebViewBuilderExtUnix::new_gtk`]. /// /// Although this methods only needs an X11 window handle, we use webkit2gtk, so you still need to initialize gtk - /// by callling `gtk::init` and advance its loop alongside your event loop using `gtk::main_iteration_do`. + /// by callling [`gtk::init`] and advance its loop alongside your event loop using [`gtk::main_iteration_do`]. /// Checkout the [Platform Considerations](https://docs.rs/wry/latest/wry/#platform-considerations) section in the crate root documentation. /// - **Windows**: The webview will auto-resize when the passed handle is resized. /// - **Linux (X11)**: Unlike macOS and Windows, the webview will not auto-resize and you'll need to call [`WebView::set_bounds`] manually. @@ -1644,7 +1644,7 @@ impl<'a> WebViewBuilder<'a> { /// # Panics: /// /// - Panics if the provided handle was not supported or invalid. - /// - Panics on Linux, if `gtk::init` was not called in this thread. + /// - Panics on Linux, if [`gtk::init`] was not called in this thread. pub fn build(self, window: &'a W) -> Result { self.error?; @@ -1662,17 +1662,17 @@ impl<'a> WebViewBuilder<'a> { /// is supported. This method won't work on Wayland. /// /// Although this methods only needs an X11 window handle, you use webkit2gtk, so you still need to initialize gtk - /// by callling `gtk::init` and advance its loop alongside your event loop using `gtk::main_iteration_do`. + /// by callling [`gtk::init`] and advance its loop alongside your event loop using [`gtk::main_iteration_do`]. /// Checkout the [Platform Considerations](https://docs.rs/wry/latest/wry/#platform-considerations) section in the crate root documentation. /// /// If you want to support child webviews on X11 and Wayland at the same time, - /// we recommend using `WebViewBuilderExtUnix::new_gtk` with `gtk::Fixed`. + /// we recommend using [`WebViewBuilderExtUnix::new_gtk`] with [`gtk::Fixed`]. /// - **Android/iOS:** Unsupported. /// /// # Panics: /// /// - Panics if the provided handle was not support or invalid. - /// - Panics on Linux, if `gtk::init` was not called in this thread. + /// - Panics on Linux, if [`gtk::init`] was not called in this thread. pub fn build_as_child(self, window: &'a W) -> Result { self.error?; @@ -1899,7 +1899,7 @@ pub trait WebViewBuilderExtWindows { /// The default value is `true`. See the following link to know more details. /// /// Setting to `false` does nothing on WebView2 Runtime version before 92.0.902.0, - /// see + /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10824-prerelease /// /// fn with_browser_accelerator_keys(self, enabled: bool) -> Self; @@ -1917,7 +1917,7 @@ pub trait WebViewBuilderExtWindows { /// Defaults to [`Theme::Auto`] which will follow the OS defaults. /// /// Requires WebView2 Runtime version 101.0.1210.39 or higher, does nothing on older versions, - /// see + /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10121039 fn with_theme(self, theme: Theme) -> Self; /// Determines whether the custom protocols should use `https://.path/to/page` instead of the default `http://.path/to/page`. @@ -1931,10 +1931,10 @@ pub trait WebViewBuilderExtWindows { /// Specifies the native scrollbar style to use with webview2. /// CSS styles that modify the scrollbar are applied on top of the native appearance configured here. /// - /// Defaults to `ScrollbarStyle::Default` which is the browser default used by Microsoft Edge. + /// Defaults to [`ScrollbarStyle::Default`] which is the browser default used by Microsoft Edge. /// /// Requires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions, - /// see + /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541 fn with_scroll_bar_style(self, style: ScrollBarStyle) -> Self; /// Determines whether the ability to install and enable extensions is enabled. @@ -1942,7 +1942,7 @@ pub trait WebViewBuilderExtWindows { /// By default, extensions are disabled. /// /// Requires WebView2 Runtime version 1.0.2210.55 or higher, does nothing on older versions, - /// see + /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10221055 fn with_browser_extensions_enabled(self, enabled: bool) -> Self; /// Set the path from which to load extensions from. Extensions stored in this path should be unpacked. @@ -2163,10 +2163,10 @@ impl WebView { /// /// # Platform-specific: /// - /// - **Linux**: Only X11 is supported, if you want to support Wayland too, use `WebViewExtUnix::new_gtk`. + /// - **Linux**: Only X11 is supported, if you want to support Wayland too, use [`WebViewExtUnix::new_gtk`]. /// /// Although this methods only needs an X11 window handle, you use webkit2gtk, so you still need to initialize gtk - /// by callling `gtk::init` and advance its loop alongside your event loop using `gtk::main_iteration_do`. + /// by callling [`gtk::init`] and advance its loop alongside your event loop using [`gtk::main_iteration_do`]. /// Checkout the [Platform Considerations](https://docs.rs/wry/latest/wry/#platform-considerations) section in the crate root documentation. /// - **macOS / Windows**: The webview will auto-resize when the passed handle is resized. /// - **Linux (X11)**: Unlike macOS and Windows, the webview will not auto-resize and you'll need to call [`WebView::set_bounds`] manually. @@ -2174,7 +2174,7 @@ impl WebView { /// # Panics: /// /// - Panics if the provided handle was not supported or invalid. - /// - Panics on Linux, if `gtk::init` was not called in this thread. + /// - Panics on Linux, if [`gtk::init`] was not called in this thread. pub fn new(window: &impl HasWindowHandle, attrs: WebViewAttributes) -> Result { WebViewBuilder::new_with_attributes(attrs).build(window) } @@ -2190,17 +2190,17 @@ impl WebView { /// is supported. This method won't work on Wayland. /// /// Although this methods only needs an X11 window handle, you use webkit2gtk, so you still need to initialize gtk - /// by callling `gtk::init` and advance its loop alongside your event loop using `gtk::main_iteration_do`. + /// by callling [`gtk::init`] and advance its loop alongside your event loop using [`gtk::main_iteration_do`]. /// Checkout the [Platform Considerations](https://docs.rs/wry/latest/wry/#platform-considerations) section in the crate root documentation. /// /// If you want to support child webviews on X11 and Wayland at the same time, - /// we recommend using `WebViewBuilderExtUnix::new_gtk` with `gtk::Fixed`. + /// we recommend using [`WebViewBuilderExtUnix::new_gtk`] with [`gtk::Fixed`]. /// - **Android/iOS:** Unsupported. /// /// # Panics: /// /// - Panics if the provided handle was not support or invalid. - /// - Panics on Linux, if `gtk::init` was not called in this thread. + /// - Panics on Linux, if [`gtk::init`] was not called in this thread. pub fn new_as_child(parent: &impl HasWindowHandle, attrs: WebViewAttributes) -> Result { WebViewBuilder::new_with_attributes(attrs).build_as_child(parent) } @@ -2361,7 +2361,7 @@ impl WebView { /// Set the webview bounds. /// /// This is only effective if the webview was created as a child - /// or created using `WebViewBuilderExtUnix::new_gtk` with `gtk::Fixed`. + /// or created using [`WebViewBuilderExtUnix::new_gtk`] with [`gtk::Fixed`]. pub fn set_bounds(&self, bounds: Rect) -> Result<()> { self.webview.set_bounds(bounds) } @@ -2452,7 +2452,7 @@ pub trait WebViewExtWindows { /// Changes the webview2 theme. /// /// Requires WebView2 Runtime version 101.0.1210.39 or higher, returns error on older versions, - /// see + /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10121039 fn set_theme(&self, theme: Theme) -> Result<()>; /// Sets the [memory usage target level][1]. From 2013b25ab6ccd993f806bfc66012e1c6ccce2bff Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 25 Feb 2026 00:17:47 +0800 Subject: [PATCH 03/26] Fix `permission_handler` example on linux --- examples/permission_handler.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/examples/permission_handler.rs b/examples/permission_handler.rs index 78f68c2ce..45026629a 100644 --- a/examples/permission_handler.rs +++ b/examples/permission_handler.rs @@ -22,7 +22,7 @@ fn main() -> wry::Result<()> { .build(&event_loop) .unwrap(); - let _webview = WebViewBuilder::new() + let builder = WebViewBuilder::new() .with_url("https://permission.site/") .with_permission_handler(|kind| { let response = match kind { @@ -31,8 +31,27 @@ fn main() -> wry::Result<()> { }; println!("[permission] {kind} → {response}"); response - }) - .build(&window)?; + }); + + #[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + ))] + let _webview = builder.build(&window)?; + #[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + )))] + let _webview = { + use tao::platform::unix::WindowExtUnix; + use wry::WebViewBuilderExtUnix; + let vbox = window.default_vbox().unwrap(); + builder.build_gtk(vbox)? + }; event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; From 2c44f16a255acf854ea852a5e03eb8f794ad191a Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 25 Feb 2026 00:34:35 +0800 Subject: [PATCH 04/26] Use type instead of string check --- src/webkitgtk/mod.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/webkitgtk/mod.rs b/src/webkitgtk/mod.rs index 18c996d5c..14be99a40 100644 --- a/src/webkitgtk/mod.rs +++ b/src/webkitgtk/mod.rs @@ -36,8 +36,9 @@ use std::{ #[cfg(any(debug_assertions, feature = "devtools"))] use webkit2gtk::WebInspectorExt; use webkit2gtk::{ - AutoplayPolicy, CookieManagerExt, InputMethodContextExt, LoadEvent, NavigationPolicyDecision, - NavigationPolicyDecisionExt, NetworkProxyMode, NetworkProxySettings, PermissionRequestExt, + AutoplayPolicy, CookieManagerExt, GeolocationPermissionRequest, InputMethodContextExt, LoadEvent, + NavigationPolicyDecision, NavigationPolicyDecisionExt, NetworkProxyMode, NetworkProxySettings, + NotificationPermissionRequest, PermissionRequestExt, PointerLockPermissionRequest, PolicyDecisionType, PrintOperationExt, SettingsExt, URIRequest, URIRequestExt, UserContentInjectedFrames, UserContentManager, UserContentManagerExt, UserMediaPermissionRequest, UserMediaPermissionRequestExt, UserScript, UserScriptInjectionTime, @@ -591,11 +592,11 @@ impl InnerWebView { // but screen sharing requests come through UserMediaPermissionRequest PermissionKind::DisplayCapture } - } else if request.type_().name() == "WebKitGeolocationPermissionRequest" { + } else if request.is::() { PermissionKind::Geolocation - } else if request.type_().name() == "WebKitNotificationPermissionRequest" { + } else if request.is::() { PermissionKind::Notifications - } else if request.type_().name() == "WebKitPointerLockPermissionRequest" { + } else if request.is::() { PermissionKind::PointerLock } else { PermissionKind::Other From f2138592c4a6564b16bc807a96d29285371cd32c Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 25 Feb 2026 00:42:44 +0800 Subject: [PATCH 05/26] Add more platform specific notes --- src/lib.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 5b5e54a50..a1dfe1845 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -548,34 +548,84 @@ pub enum PermissionKind { /// Geolocation access permission. Geolocation, /// Notifications permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS`. + /// - **Linux**: Supported via `NotificationPermissionRequest`. + /// - **macOS / Android / iOS**: Not yet supported by platform backends. Notifications, /// Clipboard read permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. ClipboardRead, /// Display capture permission (for getDisplayMedia). DisplayCapture, /// Midi access permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_MIDI_SYSTEM_EXCLUSIVE_MESSAGES`. + /// - **Android**: Supported via `android.webkit.resource.MIDI_SYSEX`. + /// - **macOS / Linux / iOS**: Not yet supported by platform backends. Midi, /// Sensors (accelerometer, gyroscope, etc.) access permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_OTHER_SENSORS`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. Sensors, /// Media key system access permission. + /// + /// ## Platform-specific + /// + /// - **Android**: Supported via `android.webkit.resource.PROTECTED_MEDIA_ID`. + /// - **Windows / macOS / Linux / iOS**: Not yet supported by platform backends. MediaKeySystemAccess, /// Local fonts access permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_LOCAL_FONTS`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. LocalFonts, /// Window management permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_WINDOW_MANAGEMENT`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. WindowManagement, /// Pointer lock permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_FILE_READ_WRITE`. + /// - **Linux**: Supported via `PointerLockPermissionRequest`. + /// - **macOS / Android / iOS**: Not yet supported by platform backends. PointerLock, /// Automatic downloads permission (multiple downloads without user interaction). + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_MULTIPLE_AUTOMATIC_DOWNLOADS`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. AutomaticDownloads, /// File system access permission (read/write via File System Access API). /// /// ## Platform-specific + /// /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_FILE_READ_WRITE`. /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. FileSystemAccess, /// Media autoplay permission. /// /// ## Platform-specific + /// /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_AUTOPLAY`. /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. Autoplay, From a0133a8101f473b20a290278bab48c56c8cd2cd8 Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 25 Feb 2026 00:45:10 +0800 Subject: [PATCH 06/26] Move permissions to a file --- src/lib.rs | 150 +-------------------------------------------- src/permissions.rs | 147 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 148 deletions(-) create mode 100644 src/permissions.rs diff --git a/src/lib.rs b/src/lib.rs index a1dfe1845..c67252cd6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -350,6 +350,7 @@ #[cfg(any(target_os = "windows", target_os = "android"))] mod custom_protocol_workaround; mod error; +mod permissions; mod proxy; #[cfg(any(target_os = "macos", target_os = "android", target_os = "ios"))] mod util; @@ -409,6 +410,7 @@ pub use cookie; pub use dpi; pub use error::*; pub use http; +pub use permissions::{PermissionKind, PermissionResponse}; pub use proxy::{ProxyConfig, ProxyEndpoint}; pub use web_context::WebContext; @@ -535,154 +537,6 @@ pub struct NewWindowFeatures { pub opener: NewWindowOpener, } -/// Permission types that can be requested by the webview. -/// -/// See [`WebViewBuilder::with_permission_handler`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum PermissionKind { - /// Microphone access permission. - Microphone, - /// Camera access permission. - Camera, - /// Geolocation access permission. - Geolocation, - /// Notifications permission. - /// - /// ## Platform-specific - /// - /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS`. - /// - **Linux**: Supported via `NotificationPermissionRequest`. - /// - **macOS / Android / iOS**: Not yet supported by platform backends. - Notifications, - /// Clipboard read permission. - /// - /// ## Platform-specific - /// - /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ`. - /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. - ClipboardRead, - /// Display capture permission (for getDisplayMedia). - DisplayCapture, - /// Midi access permission. - /// - /// ## Platform-specific - /// - /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_MIDI_SYSTEM_EXCLUSIVE_MESSAGES`. - /// - **Android**: Supported via `android.webkit.resource.MIDI_SYSEX`. - /// - **macOS / Linux / iOS**: Not yet supported by platform backends. - Midi, - /// Sensors (accelerometer, gyroscope, etc.) access permission. - /// - /// ## Platform-specific - /// - /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_OTHER_SENSORS`. - /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. - Sensors, - /// Media key system access permission. - /// - /// ## Platform-specific - /// - /// - **Android**: Supported via `android.webkit.resource.PROTECTED_MEDIA_ID`. - /// - **Windows / macOS / Linux / iOS**: Not yet supported by platform backends. - MediaKeySystemAccess, - /// Local fonts access permission. - /// - /// ## Platform-specific - /// - /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_LOCAL_FONTS`. - /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. - LocalFonts, - /// Window management permission. - /// - /// ## Platform-specific - /// - /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_WINDOW_MANAGEMENT`. - /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. - WindowManagement, - /// Pointer lock permission. - /// - /// ## Platform-specific - /// - /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_FILE_READ_WRITE`. - /// - **Linux**: Supported via `PointerLockPermissionRequest`. - /// - **macOS / Android / iOS**: Not yet supported by platform backends. - PointerLock, - /// Automatic downloads permission (multiple downloads without user interaction). - /// - /// ## Platform-specific - /// - /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_MULTIPLE_AUTOMATIC_DOWNLOADS`. - /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. - AutomaticDownloads, - /// File system access permission (read/write via File System Access API). - /// - /// ## Platform-specific - /// - /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_FILE_READ_WRITE`. - /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. - FileSystemAccess, - /// Media autoplay permission. - /// - /// ## Platform-specific - /// - /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_AUTOPLAY`. - /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. - Autoplay, - /// Other unrecognized permission type. - Other, -} - -impl std::fmt::Display for PermissionKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Microphone => write!(f, "microphone"), - Self::Camera => write!(f, "camera"), - Self::Geolocation => write!(f, "geolocation"), - Self::Notifications => write!(f, "notifications"), - Self::ClipboardRead => write!(f, "clipboard-read"), - Self::DisplayCapture => write!(f, "display-capture"), - Self::Midi => write!(f, "midi"), - Self::Sensors => write!(f, "sensors"), - Self::MediaKeySystemAccess => write!(f, "media-key-system-access"), - Self::LocalFonts => write!(f, "local-fonts"), - Self::WindowManagement => write!(f, "window-management"), - Self::PointerLock => write!(f, "pointer-lock"), - Self::AutomaticDownloads => write!(f, "automatic-downloads"), - Self::FileSystemAccess => write!(f, "file-system-access"), - Self::Autoplay => write!(f, "autoplay"), - Self::Other => write!(f, "other"), - } - } -} - -/// Response for permission requests. -/// -/// See [`WebViewBuilder::with_permission_handler`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] -pub enum PermissionResponse { - /// Grant the permission. - Allow, - /// Deny the permission. - Deny, - /// Use default behavior (show system prompt). - #[default] - Default, - /// Explicitly prompt the user (system dialog). - Prompt, -} - -impl std::fmt::Display for PermissionResponse { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Allow => write!(f, "allow"), - Self::Deny => write!(f, "deny"), - Self::Default => write!(f, "default"), - Self::Prompt => write!(f, "prompt"), - } - } -} - /// An id for a webview pub type WebViewId<'a> = &'a str; diff --git a/src/permissions.rs b/src/permissions.rs new file mode 100644 index 000000000..b0820dc42 --- /dev/null +++ b/src/permissions.rs @@ -0,0 +1,147 @@ +/// Permission types that can be requested by the webview. +/// +/// See [`crate::WebViewBuilder::with_permission_handler`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum PermissionKind { + /// Microphone access permission. + Microphone, + /// Camera access permission. + Camera, + /// Geolocation access permission. + Geolocation, + /// Notifications permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS`. + /// - **Linux**: Supported via `NotificationPermissionRequest`. + /// - **macOS / Android / iOS**: Not yet supported by platform backends. + Notifications, + /// Clipboard read permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + ClipboardRead, + /// Display capture permission (for getDisplayMedia). + DisplayCapture, + /// Midi access permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_MIDI_SYSTEM_EXCLUSIVE_MESSAGES`. + /// - **Android**: Supported via `android.webkit.resource.MIDI_SYSEX`. + /// - **macOS / Linux / iOS**: Not yet supported by platform backends. + Midi, + /// Sensors (accelerometer, gyroscope, etc.) access permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_OTHER_SENSORS`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + Sensors, + /// Media key system access permission. + /// + /// ## Platform-specific + /// + /// - **Android**: Supported via `android.webkit.resource.PROTECTED_MEDIA_ID`. + /// - **Windows / macOS / Linux / iOS**: Not yet supported by platform backends. + MediaKeySystemAccess, + /// Local fonts access permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_LOCAL_FONTS`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + LocalFonts, + /// Window management permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_WINDOW_MANAGEMENT`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + WindowManagement, + /// Pointer lock permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_FILE_READ_WRITE`. + /// - **Linux**: Supported via `PointerLockPermissionRequest`. + /// - **macOS / Android / iOS**: Not yet supported by platform backends. + PointerLock, + /// Automatic downloads permission (multiple downloads without user interaction). + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_MULTIPLE_AUTOMATIC_DOWNLOADS`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + AutomaticDownloads, + /// File system access permission (read/write via File System Access API). + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_FILE_READ_WRITE`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + FileSystemAccess, + /// Media autoplay permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_AUTOPLAY`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + Autoplay, + /// Other unrecognized permission type. + Other, +} + +impl std::fmt::Display for PermissionKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Microphone => write!(f, "microphone"), + Self::Camera => write!(f, "camera"), + Self::Geolocation => write!(f, "geolocation"), + Self::Notifications => write!(f, "notifications"), + Self::ClipboardRead => write!(f, "clipboard-read"), + Self::DisplayCapture => write!(f, "display-capture"), + Self::Midi => write!(f, "midi"), + Self::Sensors => write!(f, "sensors"), + Self::MediaKeySystemAccess => write!(f, "media-key-system-access"), + Self::LocalFonts => write!(f, "local-fonts"), + Self::WindowManagement => write!(f, "window-management"), + Self::PointerLock => write!(f, "pointer-lock"), + Self::AutomaticDownloads => write!(f, "automatic-downloads"), + Self::FileSystemAccess => write!(f, "file-system-access"), + Self::Autoplay => write!(f, "autoplay"), + Self::Other => write!(f, "other"), + } + } +} + +/// Response for permission requests. +/// +/// See [`crate::WebViewBuilder::with_permission_handler`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum PermissionResponse { + /// Grant the permission. + Allow, + /// Deny the permission. + Deny, + /// Use default behavior (show system prompt). + #[default] + Default, + /// Explicitly prompt the user (system dialog). + Prompt, +} + +impl std::fmt::Display for PermissionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Allow => write!(f, "allow"), + Self::Deny => write!(f, "deny"), + Self::Default => write!(f, "default"), + Self::Prompt => write!(f, "prompt"), + } + } +} From 09814aa2219b662b1cd2bf74da238af7e70060e7 Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 25 Feb 2026 00:51:02 +0800 Subject: [PATCH 07/26] Add notice about prompt not supported on linux --- src/permissions.rs | 8 ++++++++ src/webkitgtk/mod.rs | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/permissions.rs b/src/permissions.rs index b0820dc42..df4e33628 100644 --- a/src/permissions.rs +++ b/src/permissions.rs @@ -129,9 +129,17 @@ pub enum PermissionResponse { /// Deny the permission. Deny, /// Use default behavior (show system prompt). + /// + /// ## Platform-specific + /// + /// - **Linux**: The default behavior is [`Self::Deny`] #[default] Default, /// Explicitly prompt the user (system dialog). + /// + /// ## Platform-specific + /// + /// - **Linux**: Not supported, same as [`Self::Deny`] Prompt, } diff --git a/src/webkitgtk/mod.rs b/src/webkitgtk/mod.rs index 14be99a40..a416a5591 100644 --- a/src/webkitgtk/mod.rs +++ b/src/webkitgtk/mod.rs @@ -616,7 +616,7 @@ impl InnerWebView { true // handled } PermissionResponse::Default | PermissionResponse::Prompt => { - false // not handled, let WebKitGTK show default prompt + false // not handled, `PermissionResponse::Prompt` is not supported yet } } }); From 84e01f4daa75fc21fabe8e2b2e3f9b13178a9a0d Mon Sep 17 00:00:00 2001 From: F0RLE Date: Mon, 6 Apr 2026 21:57:00 +0300 Subject: [PATCH 08/26] fix(webkitgtk): handle combined audio+video permissions and use is_for_display_device on v2_42 --- src/webkitgtk/mod.rs | 94 ++++++++++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 26 deletions(-) diff --git a/src/webkitgtk/mod.rs b/src/webkitgtk/mod.rs index a416a5591..3b657deb2 100644 --- a/src/webkitgtk/mod.rs +++ b/src/webkitgtk/mod.rs @@ -580,19 +580,66 @@ impl InnerWebView { // Permission handler if let Some(permission_handler) = attributes.permission_handler.take() { webview.connect_permission_request(move |_webview, request| { - // Determine permission kind - let permission_kind = - if let Some(media_request) = request.downcast_ref::() { - if media_request.is_for_audio_device() { - PermissionKind::Microphone - } else if media_request.is_for_video_device() { - PermissionKind::Camera + if let Some(media_request) = request.downcast_ref::() { + let is_audio = media_request.is_for_audio_device(); + let is_video = media_request.is_for_video_device(); + + #[cfg(feature = "v2_42")] + let is_display = media_request.is_for_display_device(); + #[cfg(not(feature = "v2_42"))] + let is_display = !is_audio && !is_video; + + if is_display { + // Screen sharing request + let response = permission_handler(PermissionKind::DisplayCapture); + return match response { + PermissionResponse::Allow => { + request.allow(); + true + } + PermissionResponse::Deny => { + request.deny(); + true + } + PermissionResponse::Default | PermissionResponse::Prompt => false, + }; + } + + // For combined audio+video requests, check each individually. + // Deny wins: if either is denied, deny the whole request. + let mut allow = true; + let mut handled = false; + + if is_audio { + handled = true; + match permission_handler(PermissionKind::Microphone) { + PermissionResponse::Allow => {} + PermissionResponse::Deny => allow = false, + PermissionResponse::Default | PermissionResponse::Prompt => handled = false, + } + } + + if is_video && allow { + handled = true; + match permission_handler(PermissionKind::Camera) { + PermissionResponse::Allow => {} + PermissionResponse::Deny => allow = false, + PermissionResponse::Default | PermissionResponse::Prompt => handled = false, + } + } + + if handled { + if allow { + request.allow(); } else { - // On WebKitGTK < 2.42, there's no is_for_display_device() - // but screen sharing requests come through UserMediaPermissionRequest - PermissionKind::DisplayCapture + request.deny(); } - } else if request.is::() { + true + } else { + false // let WebKitGTK show default prompt + } + } else { + let permission_kind = if request.is::() { PermissionKind::Geolocation } else if request.is::() { PermissionKind::Notifications @@ -602,21 +649,16 @@ impl InnerWebView { PermissionKind::Other }; - // Call user's permission handler - let response = permission_handler(permission_kind); - - // Apply the response - match response { - PermissionResponse::Allow => { - request.allow(); - true // handled - } - PermissionResponse::Deny => { - request.deny(); - true // handled - } - PermissionResponse::Default | PermissionResponse::Prompt => { - false // not handled, `PermissionResponse::Prompt` is not supported yet + match permission_handler(permission_kind) { + PermissionResponse::Allow => { + request.allow(); + true + } + PermissionResponse::Deny => { + request.deny(); + true + } + PermissionResponse::Default | PermissionResponse::Prompt => false, } } }); From 911de4190a8fda1716761643136008006667c1ed Mon Sep 17 00:00:00 2001 From: F0RLE Date: Tue, 7 Apr 2026 22:03:33 +0300 Subject: [PATCH 09/26] fix: reorder module declarations for rustfmt --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 57d1ad5e6..22eee234f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -350,9 +350,9 @@ #[cfg(any(target_os = "windows", target_os = "android"))] mod custom_protocol_workaround; mod error; -mod permissions; #[cfg(any(target_os = "android", test))] mod inject_initialization_scripts; +mod permissions; mod proxy; #[cfg(any(target_os = "macos", target_os = "android", target_os = "ios"))] mod util; From 47afa95c2f23475b24df559a518069a1062a765f Mon Sep 17 00:00:00 2001 From: F0RLE Date: Thu, 9 Apr 2026 16:23:27 +0300 Subject: [PATCH 10/26] fix(android): scope permission handler per webview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit подтверждено --- src/android/binding.rs | 24 +++++++++++++++-------- src/android/kotlin/RustWebChromeClient.kt | 4 ++-- src/android/kotlin/WryActivity.kt | 4 ++++ src/android/mod.rs | 10 ++++++++++ 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/android/binding.rs b/src/android/binding.rs index bef74613b..4719bfefe 100644 --- a/src/android/binding.rs +++ b/src/android/binding.rs @@ -92,7 +92,7 @@ macro_rules! android_binding { $package, RustWebChromeClient, onPermissionRequestNative, - [jni::objects::JObjectArray], + [JString, jni::objects::JObjectArray], jint ); }}; @@ -490,14 +490,24 @@ pub unsafe fn onPageLoaded(mut env: JNIEnv, _: JClass, webview_id: JString, url: } } +#[allow(non_snake_case)] pub unsafe fn onPermissionRequestNative( mut env: JNIEnv, _: JClass, + webview_id: JString, resources: jni::objects::JObjectArray, ) -> jint { let mut allowed = false; let mut denied = false; let mut prompt = false; + let Ok(webview_id) = env.get_string(&webview_id) else { + return 2; + }; + let webview_id = webview_id.to_str().ok().unwrap_or_default(); + let permission_handlers = PERMISSION_HANDLER.lock().unwrap(); + let Some(handler) = permission_handlers.get(webview_id) else { + return 2; + }; if let Ok(size) = env.get_array_length(&resources) { for i in 0..size { @@ -513,13 +523,11 @@ pub unsafe fn onPermissionRequestNative( _ => PermissionKind::Other, }; - if let Some(handler) = &*PERMISSION_HANDLER.lock().unwrap() { - match (handler.handler)(kind) { - PermissionResponse::Allow => allowed = true, - PermissionResponse::Deny => denied = true, - PermissionResponse::Prompt => prompt = true, - PermissionResponse::Default => {} - } + match (handler.handler)(kind) { + PermissionResponse::Allow => allowed = true, + PermissionResponse::Deny => denied = true, + PermissionResponse::Prompt => prompt = true, + PermissionResponse::Default => {} } } } diff --git a/src/android/kotlin/RustWebChromeClient.kt b/src/android/kotlin/RustWebChromeClient.kt index 533e4bbdc..3f45122b6 100644 --- a/src/android/kotlin/RustWebChromeClient.kt +++ b/src/android/kotlin/RustWebChromeClient.kt @@ -92,7 +92,7 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { } override fun onPermissionRequest(request: PermissionRequest) { - val response = onPermissionRequestNative(request.resources) + val response = onPermissionRequestNative(activity.currentWebViewId(), request.resources) when (response) { 0 -> { // Allow request.grant(request.resources) @@ -136,7 +136,7 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { } } - private external fun onPermissionRequestNative(resources: Array): Int + private external fun onPermissionRequestNative(webviewId: String, resources: Array): Int /** * Show the browser alert modal diff --git a/src/android/kotlin/WryActivity.kt b/src/android/kotlin/WryActivity.kt index 7152e352b..070d57766 100644 --- a/src/android/kotlin/WryActivity.kt +++ b/src/android/kotlin/WryActivity.kt @@ -76,6 +76,10 @@ abstract class WryActivity : AppCompatActivity() { onWebViewCreate(webView) } + fun currentWebViewId(): String { + return if (::mWebView.isInitialized) mWebView.id else "" + } + val version: String @SuppressLint("WebViewApiAvailability", "ObsoleteSdkInt") get() { diff --git a/src/android/mod.rs b/src/android/mod.rs index 22b9ee168..2e3d1abb5 100644 --- a/src/android/mod.rs +++ b/src/android/mod.rs @@ -96,6 +96,7 @@ pub fn destroy_webview(activity_id: ActivityId, webview_id: &WebviewId) { TITLE_CHANGE_HANDLER.lock().unwrap().remove(webview_id); URL_LOADING_OVERRIDE.lock().unwrap().remove(webview_id); ON_LOAD_HANDLER.lock().unwrap().remove(webview_id); + PERMISSION_HANDLER.lock().unwrap().remove(webview_id); WITH_ASSET_LOADER.lock().unwrap().remove(webview_id); ASSET_LOADER_DOMAIN.lock().unwrap().remove(webview_id); } @@ -316,6 +317,15 @@ impl InnerWebView { .insert(id.clone(), UnsafeOnPageLoadHandler::new(h)); } + if let Some(permission_handler) = attributes.permission_handler { + let permission_handler: Box PermissionResponse> = + permission_handler; + PERMISSION_HANDLER + .lock() + .unwrap() + .insert(id.clone(), UnsafePermissionHandler::new(permission_handler)); + } + let attributes = CreateWebViewAttributes { id: id.clone(), url, From b9d86ef77ba33cfc719c3ad7aab61ba0e8f564e3 Mon Sep 17 00:00:00 2001 From: F0RLE Date: Thu, 9 Apr 2026 18:08:41 +0300 Subject: [PATCH 11/26] fix(android): honor permission handler for geolocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit подтверждено --- src/android/binding.rs | 32 +++++++++++++ src/android/kotlin/RustWebChromeClient.kt | 55 +++++++++++++++++++---- src/lib.rs | 4 +- src/permissions.rs | 7 +++ 4 files changed, 89 insertions(+), 9 deletions(-) diff --git a/src/android/binding.rs b/src/android/binding.rs index 4719bfefe..59c1af26c 100644 --- a/src/android/binding.rs +++ b/src/android/binding.rs @@ -95,6 +95,14 @@ macro_rules! android_binding { [JString, jni::objects::JObjectArray], jint ); + android_fn!( + $domain, + $package, + RustWebChromeClient, + onGeolocationPermissionRequestNative, + [JString, JString], + jint + ); }}; } @@ -545,3 +553,27 @@ pub unsafe fn onPermissionRequestNative( 2 // Default } } + +#[allow(non_snake_case)] +pub unsafe fn onGeolocationPermissionRequestNative( + mut env: JNIEnv, + _: JClass, + webview_id: JString, + _origin: JString, +) -> jint { + let Ok(webview_id) = env.get_string(&webview_id) else { + return 2; + }; + let webview_id = webview_id.to_str().ok().unwrap_or_default(); + let permission_handlers = PERMISSION_HANDLER.lock().unwrap(); + let Some(handler) = permission_handlers.get(webview_id) else { + return 2; + }; + + match (handler.handler)(PermissionKind::Geolocation) { + PermissionResponse::Allow => 0, + PermissionResponse::Deny => 1, + PermissionResponse::Default => 2, + PermissionResponse::Prompt => 3, + } +} diff --git a/src/android/kotlin/RustWebChromeClient.kt b/src/android/kotlin/RustWebChromeClient.kt index 3f45122b6..3b7f7f1d8 100644 --- a/src/android/kotlin/RustWebChromeClient.kt +++ b/src/android/kotlin/RustWebChromeClient.kt @@ -92,10 +92,15 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { } override fun onPermissionRequest(request: PermissionRequest) { + val requestedResources = safePermissionRequestResources(request.resources) val response = onPermissionRequestNative(activity.currentWebViewId(), request.resources) when (response) { 0 -> { // Allow - request.grant(request.resources) + if (requestedResources.isNotEmpty()) { + request.grant(requestedResources) + } else { + request.deny() + } return } 1 -> { // Deny @@ -112,19 +117,21 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M val permissionList: MutableList = ArrayList() - if (listOf(*request.resources).contains("android.webkit.resource.VIDEO_CAPTURE")) { + if (requestedResources.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) { permissionList.add(Manifest.permission.CAMERA) } - if (listOf(*request.resources).contains("android.webkit.resource.AUDIO_CAPTURE")) { + if (requestedResources.contains(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) { permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS) permissionList.add(Manifest.permission.RECORD_AUDIO) } - if (permissionList.isNotEmpty() && isRequestPermissionRequired) { + if (requestedResources.isEmpty()) { + request.deny() + } else if (permissionList.isNotEmpty() && isRequestPermissionRequired) { val permissions = permissionList.toTypedArray() permissionListener = object : PermissionListener { override fun onPermissionSelect(isGranted: Boolean?) { if (isGranted == true) { - request.grant(request.resources) + request.grant(requestedResources) } else { request.deny() } @@ -132,11 +139,12 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { } permissionLauncher.launch(permissions) } else { - request.grant(request.resources) + request.grant(requestedResources) } } private external fun onPermissionRequestNative(webviewId: String, resources: Array): Int + private external fun onGeolocationPermissionRequestNative(webviewId: String, origin: String): Int /** * Show the browser alert modal @@ -260,9 +268,20 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { callback: GeolocationPermissions.Callback ) { super.onGeolocationPermissionsShowPrompt(origin, callback) + when (onGeolocationPermissionRequestNative(activity.currentWebViewId(), origin)) { + 1 -> { + callback.invoke(origin, false, false) + return + } + } + Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: $origin") - val geoPermissions = - arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) + val geoPermissions = definedGeolocationPermissions() + if (geoPermissions.isEmpty()) { + callback.invoke(origin, false, false) + return + } + if (!PermissionHelper.hasPermissions(activity, geoPermissions)) { permissionListener = object : PermissionListener { override fun onPermissionSelect(isGranted: Boolean?) { @@ -289,6 +308,26 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { } } + private fun safePermissionRequestResources(resources: Array): Array { + return resources.filter { + it == PermissionRequest.RESOURCE_AUDIO_CAPTURE || + it == PermissionRequest.RESOURCE_VIDEO_CAPTURE || + it == PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID || + it == PermissionRequest.RESOURCE_MIDI_SYSEX + }.toTypedArray() + } + + private fun definedGeolocationPermissions(): Array { + val permissions = ArrayList() + if (PermissionHelper.hasDefinedPermission(activity, Manifest.permission.ACCESS_COARSE_LOCATION)) { + permissions.add(Manifest.permission.ACCESS_COARSE_LOCATION) + } + if (PermissionHelper.hasDefinedPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION)) { + permissions.add(Manifest.permission.ACCESS_FINE_LOCATION) + } + return permissions.toTypedArray() + } + override fun onShowFileChooser( webView: WebView, filePathCallback: ValueCallback?>, diff --git a/src/lib.rs b/src/lib.rs index 22eee234f..2a4238368 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -808,7 +808,9 @@ struct WebViewAttributes<'a> { /// - **Windows**: Fully supported via WebView2's PermissionRequested event. /// - **macOS / iOS**: Fully supported via WKUIDelegate's requestMediaCapturePermission. /// - **Linux**: Fully supported via WebKitGTK's permission-request signal. - /// - **Android**: Supported via JNI bridge with some limitations (WIP). + /// - **Android**: Supported via JNI bridge for geolocation, microphone, camera, + /// protected media, and MIDI requests. Android runtime permissions may still + /// trigger native OS prompts before access is granted. /// /// ## Example /// diff --git a/src/permissions.rs b/src/permissions.rs index df4e33628..ea6416418 100644 --- a/src/permissions.rs +++ b/src/permissions.rs @@ -9,6 +9,13 @@ pub enum PermissionKind { /// Camera access permission. Camera, /// Geolocation access permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION`. + /// - **Linux**: Supported via `GeolocationPermissionRequest`. + /// - **Android**: Supported via `WebChromeClient.onGeolocationPermissionsShowPrompt`. + /// - **macOS / iOS**: Not yet supported by platform backends. Geolocation, /// Notifications permission. /// From e25436ddb83784d13280f92062e316c82834f523 Mon Sep 17 00:00:00 2001 From: F0RLE Date: Thu, 23 Apr 2026 23:11:32 +0300 Subject: [PATCH 12/26] fix(android): avoid overgranting mixed permission requests --- src/android/kotlin/RustWebChromeClient.kt | 75 ++++++++++++++--------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/src/android/kotlin/RustWebChromeClient.kt b/src/android/kotlin/RustWebChromeClient.kt index 3b7f7f1d8..48b741d11 100644 --- a/src/android/kotlin/RustWebChromeClient.kt +++ b/src/android/kotlin/RustWebChromeClient.kt @@ -93,45 +93,50 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { override fun onPermissionRequest(request: PermissionRequest) { val requestedResources = safePermissionRequestResources(request.resources) - val response = onPermissionRequestNative(activity.currentWebViewId(), request.resources) - when (response) { - 0 -> { // Allow - if (requestedResources.isNotEmpty()) { - request.grant(requestedResources) - } else { + if (requestedResources.isEmpty()) { + request.deny() + return + } + + val grantedResources = ArrayList() + val defaultResources = ArrayList() + + for (resource in requestedResources) { + when (onPermissionRequestNative(activity.currentWebViewId(), arrayOf(resource))) { + 0 -> grantedResources.add(resource) + 1 -> { request.deny() + return } - return - } - 1 -> { // Deny - request.deny() - return - } - 2 -> { // Default - // Continue with default logic - } - 3 -> { // Prompt - // Continue with default logic (which prompts) + 2, 3 -> defaultResources.add(resource) } } - val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M - val permissionList: MutableList = ArrayList() - if (requestedResources.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) { - permissionList.add(Manifest.permission.CAMERA) - } - if (requestedResources.contains(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) { - permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS) - permissionList.add(Manifest.permission.RECORD_AUDIO) + if (grantedResources.isNotEmpty()) { + // Android PermissionRequest can only be completed once. When the handler + // explicitly allows a subset and leaves the rest as default/prompt, grant + // only the handled subset and let the remaining resources be denied. + grantPermissionRequest(request, grantedResources.toTypedArray()) + return } - if (requestedResources.isEmpty()) { + + grantPermissionRequest(request, defaultResources.toTypedArray()) + } + + private fun grantPermissionRequest(request: PermissionRequest, resources: Array) { + if (resources.isEmpty()) { request.deny() - } else if (permissionList.isNotEmpty() && isRequestPermissionRequired) { + return + } + + val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + val permissionList = androidPermissionsForResources(resources) + if (permissionList.isNotEmpty() && isRequestPermissionRequired) { val permissions = permissionList.toTypedArray() permissionListener = object : PermissionListener { override fun onPermissionSelect(isGranted: Boolean?) { if (isGranted == true) { - request.grant(requestedResources) + request.grant(resources) } else { request.deny() } @@ -139,8 +144,20 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { } permissionLauncher.launch(permissions) } else { - request.grant(requestedResources) + request.grant(resources) + } + } + + private fun androidPermissionsForResources(resources: Array): MutableList { + val permissionList: MutableList = ArrayList() + if (resources.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) { + permissionList.add(Manifest.permission.CAMERA) + } + if (resources.contains(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) { + permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS) + permissionList.add(Manifest.permission.RECORD_AUDIO) } + return permissionList } private external fun onPermissionRequestNative(webviewId: String, resources: Array): Int From 954e3e6dc60886aaa7b05cbdf2d01942d48ded52 Mon Sep 17 00:00:00 2001 From: F0RLE Date: Sat, 25 Apr 2026 10:49:13 +0300 Subject: [PATCH 13/26] fix(android): scope permission callbacks by webview --- src/android/kotlin/RustWebChromeClient.kt | 156 +++++++--------------- src/android/kotlin/WryActivity.kt | 39 +++++- src/android/main_pipe.rs | 35 ++--- src/android/mod.rs | 20 +-- src/permissions.rs | 3 +- 5 files changed, 105 insertions(+), 148 deletions(-) diff --git a/src/android/kotlin/RustWebChromeClient.kt b/src/android/kotlin/RustWebChromeClient.kt index 48b741d11..416349060 100644 --- a/src/android/kotlin/RustWebChromeClient.kt +++ b/src/android/kotlin/RustWebChromeClient.kt @@ -21,53 +21,13 @@ import android.provider.MediaStore import android.view.View import android.webkit.* import android.widget.EditText -import androidx.activity.result.ActivityResult -import androidx.activity.result.ActivityResultCallback -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.FileProvider import java.io.File import java.io.IOException import java.text.SimpleDateFormat import java.util.* -class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { - private interface PermissionListener { - fun onPermissionSelect(isGranted: Boolean?) - } - - private interface ActivityResultListener { - fun onActivityResult(result: ActivityResult?) - } - - private val activity: WryActivity - private var permissionLauncher: ActivityResultLauncher> - private var activityLauncher: ActivityResultLauncher - private var permissionListener: PermissionListener? = null - private var activityListener: ActivityResultListener? = null - - init { - activity = appActivity - val permissionCallback = - ActivityResultCallback { isGranted: Map -> - if (permissionListener != null) { - var granted = true - for ((_, value) in isGranted) { - if (!value) granted = false - } - permissionListener!!.onPermissionSelect(granted) - } - } - permissionLauncher = - activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions(), permissionCallback) - activityLauncher = activity.registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - if (activityListener != null) { - activityListener!!.onActivityResult(result) - } - } - } +class RustWebChromeClient(private val activity: WryActivity, private val webViewId: String) : WebChromeClient() { /** * Render web content in `view`. @@ -102,7 +62,7 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { val defaultResources = ArrayList() for (resource in requestedResources) { - when (onPermissionRequestNative(activity.currentWebViewId(), arrayOf(resource))) { + when (onPermissionRequestNative(webViewId, arrayOf(resource))) { 0 -> grantedResources.add(resource) 1 -> { request.deny() @@ -133,16 +93,13 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { val permissionList = androidPermissionsForResources(resources) if (permissionList.isNotEmpty() && isRequestPermissionRequired) { val permissions = permissionList.toTypedArray() - permissionListener = object : PermissionListener { - override fun onPermissionSelect(isGranted: Boolean?) { - if (isGranted == true) { - request.grant(resources) - } else { - request.deny() - } + activity.requestPermissions(permissions) { isGranted -> + if (isGranted == true) { + request.grant(resources) + } else { + request.deny() } } - permissionLauncher.launch(permissions) } else { request.grant(resources) } @@ -285,7 +242,7 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { callback: GeolocationPermissions.Callback ) { super.onGeolocationPermissionsShowPrompt(origin, callback) - when (onGeolocationPermissionRequestNative(activity.currentWebViewId(), origin)) { + when (onGeolocationPermissionRequestNative(webViewId, origin)) { 1 -> { callback.invoke(origin, false, false) return @@ -300,24 +257,21 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { } if (!PermissionHelper.hasPermissions(activity, geoPermissions)) { - permissionListener = object : PermissionListener { - override fun onPermissionSelect(isGranted: Boolean?) { - if (isGranted == true) { + activity.requestPermissions(geoPermissions) { isGranted -> + if (isGranted == true) { + callback.invoke(origin, true, false) + } else { + val coarsePermission = + arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + PermissionHelper.hasPermissions(activity, coarsePermission) + ) { callback.invoke(origin, true, false) } else { - val coarsePermission = - arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && - PermissionHelper.hasPermissions(activity, coarsePermission) - ) { - callback.invoke(origin, true, false) - } else { - callback.invoke(origin, false, false) - } + callback.invoke(origin, false, false) } } } - permissionLauncher.launch(geoPermissions) } else { // permission is already granted callback.invoke(origin, true, false) @@ -358,18 +312,15 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { if (isMediaCaptureSupported) { showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo) } else { - permissionListener = object : PermissionListener { - override fun onPermissionSelect(isGranted: Boolean?) { - if (isGranted == true) { - showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo) - } else { - Logger.warn(Logger.tags("FileChooser"), "Camera permission not granted") - filePathCallback.onReceiveValue(null) - } + val camPermission = arrayOf(Manifest.permission.CAMERA) + activity.requestPermissions(camPermission) { isGranted -> + if (isGranted == true) { + showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo) + } else { + Logger.warn(Logger.tags("FileChooser"), "Camera permission not granted") + filePathCallback.onReceiveValue(null) } } - val camPermission = arrayOf(Manifest.permission.CAMERA) - permissionLauncher.launch(camPermission) } } else { showFilePicker(filePathCallback, fileChooserParams) @@ -416,16 +367,13 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { return false } takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri) - activityListener = object : ActivityResultListener { - override fun onActivityResult(result: ActivityResult?) { - var res: Array? = null - if (result?.resultCode == Activity.RESULT_OK) { - res = arrayOf(imageFileUri) - } - filePathCallback.onReceiveValue(res) + activity.launchActivityForResult(takePictureIntent) { result -> + var res: Array? = null + if (result?.resultCode == Activity.RESULT_OK) { + res = arrayOf(imageFileUri) } + filePathCallback.onReceiveValue(res) } - activityLauncher.launch(takePictureIntent) return true } @@ -434,16 +382,13 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { if (takeVideoIntent.resolveActivity(activity.packageManager) == null) { return false } - activityListener = object : ActivityResultListener { - override fun onActivityResult(result: ActivityResult?) { - var res: Array? = null - if (result?.resultCode == Activity.RESULT_OK) { - res = arrayOf(result.data!!.data) - } - filePathCallback.onReceiveValue(res) + activity.launchActivityForResult(takeVideoIntent) { result -> + var res: Array? = null + if (result?.resultCode == Activity.RESULT_OK) { + res = arrayOf(result.data!!.data) } + filePathCallback.onReceiveValue(res) } - activityLauncher.launch(takeVideoIntent) return true } @@ -463,26 +408,23 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { } } try { - activityListener = object : ActivityResultListener { - override fun onActivityResult(result: ActivityResult?) { - val res: Array? - val resultIntent = result?.data - if (result?.resultCode == Activity.RESULT_OK && resultIntent!!.clipData != null) { - val numFiles = resultIntent.clipData!!.itemCount - res = arrayOfNulls(numFiles) - for (i in 0 until numFiles) { - res[i] = resultIntent.clipData!!.getItemAt(i).uri - } - } else { - res = FileChooserParams.parseResult( - result?.resultCode ?: 0, - resultIntent - ) + activity.launchActivityForResult(intent) { result -> + val res: Array? + val resultIntent = result?.data + if (result?.resultCode == Activity.RESULT_OK && resultIntent!!.clipData != null) { + val numFiles = resultIntent.clipData!!.itemCount + res = arrayOfNulls(numFiles) + for (i in 0 until numFiles) { + res[i] = resultIntent.clipData!!.getItemAt(i).uri } - filePathCallback.onReceiveValue(res) + } else { + res = FileChooserParams.parseResult( + result?.resultCode ?: 0, + resultIntent + ) } + filePathCallback.onReceiveValue(res) } - activityLauncher.launch(intent) } catch (e: ActivityNotFoundException) { filePathCallback.onReceiveValue(null) } diff --git a/src/android/kotlin/WryActivity.kt b/src/android/kotlin/WryActivity.kt index 070d57766..9fe255468 100644 --- a/src/android/kotlin/WryActivity.kt +++ b/src/android/kotlin/WryActivity.kt @@ -11,6 +11,10 @@ import android.os.Bundle import android.webkit.WebView import android.view.KeyEvent import androidx.activity.OnBackPressedCallback +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner @@ -48,11 +52,25 @@ object WryLifecycleObserver : DefaultLifecycleObserver { abstract class WryActivity : AppCompatActivity() { private lateinit var mWebView: RustWebView + private lateinit var permissionLauncher: ActivityResultLauncher> + private lateinit var activityLauncher: ActivityResultLauncher + private var permissionListener: ((Boolean?) -> Unit)? = null + private var activityListener: ((ActivityResult?) -> Unit)? = null var id: Int = 0 open val handleBackNavigation: Boolean = true open fun onWebViewCreate(webView: WebView) { } + fun requestPermissions(permissions: Array, listener: (Boolean?) -> Unit) { + permissionListener = listener + permissionLauncher.launch(permissions) + } + + fun launchActivityForResult(intent: Intent, listener: (ActivityResult?) -> Unit) { + activityListener = listener + activityLauncher.launch(intent) + } + fun setWebView(webView: RustWebView) { mWebView = webView @@ -76,10 +94,6 @@ abstract class WryActivity : AppCompatActivity() { onWebViewCreate(webView) } - fun currentWebViewId(): String { - return if (::mWebView.isInitialized) mWebView.id else "" - } - val version: String @SuppressLint("WebViewApiAvailability", "ObsoleteSdkInt") get() { @@ -116,6 +130,23 @@ abstract class WryActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) id = savedInstanceState?.getInt(ACTIVITY_ID_KEY) ?: intent.extras?.getInt(ACTIVITY_ID_KEY) ?: hashCode() + val permissionCallback = + ActivityResultCallback { isGranted: Map -> + permissionListener?.let { + var granted = true + for ((_, value) in isGranted) { + if (!value) granted = false + } + it(granted) + } + } + permissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions(), permissionCallback) + activityLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + activityListener?.invoke(result) + } ProcessLifecycleOwner.get().lifecycle.addObserver(WryLifecycleObserver) Rust.onActivityCreate(this) } diff --git a/src/android/main_pipe.rs b/src/android/main_pipe.rs index 52308fd86..166f8f360 100644 --- a/src/android/main_pipe.rs +++ b/src/android/main_pipe.rs @@ -36,24 +36,17 @@ pub struct ActivityProxy { pub activity: GlobalRef, pub window_manager: GlobalRef, pub webview: Option, - pub webchrome_client: GlobalRef, pub java_vm: *mut c_void, } unsafe impl Send for ActivityProxy {} impl ActivityProxy { - pub fn new( - vm: JavaVM, - activity: GlobalRef, - window_manager: GlobalRef, - webchrome_client: GlobalRef, - ) -> Self { + pub fn new(vm: JavaVM, activity: GlobalRef, window_manager: GlobalRef) -> Self { Self { activity, window_manager, webview: None, - webchrome_client, java_vm: vm.get_java_vm_pointer() as *mut _, } } @@ -75,16 +68,14 @@ pub fn register_activity_proxy( id: ActivityId, activity: GlobalRef, window_manager: GlobalRef, - webchrome_client: GlobalRef, ) { let mut activity_proxy = ACTIVITY_PROXY.lock().unwrap(); if let Some(proxy) = activity_proxy.get_mut(&id) { proxy.activity = activity; proxy.window_manager = window_manager; - proxy.webchrome_client = webchrome_client; proxy.java_vm = vm.get_java_vm_pointer() as *mut _; } else { - let proxy = ActivityProxy::new(vm, activity, window_manager, webchrome_client); + let proxy = ActivityProxy::new(vm, activity, window_manager); activity_proxy.insert(id, proxy.clone()); } } @@ -146,9 +137,7 @@ impl<'a> MainPipe<'a> { if let Ok((activity_id, message)) = CHANNEL.1.recv() { match message { WebViewMessage::CreateWebView(attrs) => { - let Some((activity, web_chrome_client)) = - activity_proxy(activity_id).map(|p| (p.activity.clone(), p.webchrome_client.clone())) - else { + let Some(activity) = activity_proxy(activity_id).map(|p| p.activity.clone()) else { #[cfg(debug_assertions)] eprintln!("no activity found for activity id: {}", activity_id); return Ok(()); @@ -282,12 +271,26 @@ impl<'a> MainPipe<'a> { "(Landroid/webkit/WebViewClient;)V", &[(&webview_client).into()], )?; - // set webchrome client + // Create and set webchrome client + let rust_webchrome_client_class = find_class( + &mut self.env, + &activity, + format!("{}/RustWebChromeClient", PACKAGE.get().unwrap()), + )?; + let webview_id = self.env.new_string(&id)?; + let web_chrome_client = self.env.new_object( + &rust_webchrome_client_class, + format!( + "(L{}/WryActivity;Ljava/lang/String;)V", + PACKAGE.get().unwrap() + ), + &[(&activity).into(), (&webview_id).into()], + )?; self.env.call_method( &webview, "setWebChromeClient", "(Landroid/webkit/WebChromeClient;)V", - &[web_chrome_client.as_obj().into()], + &[(&web_chrome_client).into()], )?; // Add javascript interface (IPC) diff --git a/src/android/mod.rs b/src/android/mod.rs index 2e3d1abb5..85837d71e 100644 --- a/src/android/mod.rs +++ b/src/android/mod.rs @@ -133,25 +133,7 @@ pub unsafe fn android_setup( .unwrap(); let window_manager = env.new_global_ref(window_manager).unwrap(); - // we must create the WebChromeClient here because it calls `registerForActivityResult`, - // which gives an `LifecycleOwners must call register before they are STARTED.` error when called outside the onCreate hook - let rust_webchrome_client_class = find_class( - &mut env, - activity.as_obj(), - format!("{}/RustWebChromeClient", PACKAGE.get().unwrap()), - ) - .unwrap(); - let webchrome_client = env - .new_object( - &rust_webchrome_client_class, - format!("(L{}/WryActivity;)V", PACKAGE.get().unwrap()), - &[activity.as_obj().into()], - ) - .unwrap(); - - let webchrome_client = env.new_global_ref(webchrome_client).unwrap(); - - register_activity_proxy(vm, activity_id, activity, window_manager, webchrome_client); + register_activity_proxy(vm, activity_id, activity, window_manager); if let Some(webview_attributes) = WEBVIEW_ATTRIBUTES.lock().unwrap().get(&activity_id) { MainPipe::send( diff --git a/src/permissions.rs b/src/permissions.rs index ea6416418..84363a3c7 100644 --- a/src/permissions.rs +++ b/src/permissions.rs @@ -74,9 +74,8 @@ pub enum PermissionKind { /// /// ## Platform-specific /// - /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_FILE_READ_WRITE`. /// - **Linux**: Supported via `PointerLockPermissionRequest`. - /// - **macOS / Android / iOS**: Not yet supported by platform backends. + /// - **Windows / macOS / Android / iOS**: Not yet supported by platform backends. PointerLock, /// Automatic downloads permission (multiple downloads without user interaction). /// From 88472aedf318287eddd832cc3e2217cde426f766 Mon Sep 17 00:00:00 2001 From: F0RLE Date: Sat, 25 Apr 2026 14:37:56 +0300 Subject: [PATCH 14/26] fix(android): pass existing webview id string --- src/android/main_pipe.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/android/main_pipe.rs b/src/android/main_pipe.rs index 166f8f360..de232a325 100644 --- a/src/android/main_pipe.rs +++ b/src/android/main_pipe.rs @@ -277,14 +277,13 @@ impl<'a> MainPipe<'a> { &activity, format!("{}/RustWebChromeClient", PACKAGE.get().unwrap()), )?; - let webview_id = self.env.new_string(&id)?; let web_chrome_client = self.env.new_object( &rust_webchrome_client_class, format!( "(L{}/WryActivity;Ljava/lang/String;)V", PACKAGE.get().unwrap() ), - &[(&activity).into(), (&webview_id).into()], + &[(&activity).into(), (&id).into()], )?; self.env.call_method( &webview, From f00e365aed395613103e660f963336cedf2b0e4c Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 10 Jun 2026 16:48:46 +0800 Subject: [PATCH 15/26] Migrate `PACKAGE.get().unwrap()` --- src/android/main_pipe.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/android/main_pipe.rs b/src/android/main_pipe.rs index 39d506f4e..adb54cc2f 100644 --- a/src/android/main_pipe.rs +++ b/src/android/main_pipe.rs @@ -279,14 +279,11 @@ impl<'a> MainPipe<'a> { let rust_webchrome_client_class = find_class( &mut self.env, &activity, - format!("{}/RustWebChromeClient", PACKAGE.get().unwrap()), + format!("{package}/RustWebChromeClient"), )?; let web_chrome_client = self.env.new_object( &rust_webchrome_client_class, - format!( - "(L{}/WryActivity;Ljava/lang/String;)V", - PACKAGE.get().unwrap() - ), + format!("(L{package}/WryActivity;Ljava/lang/String;)V"), &[(&activity).into(), (&id).into()], )?; self.env.call_method( From 059df14e9d64f33ad1feaec681ae750f5155790a Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 10 Jun 2026 16:52:36 +0800 Subject: [PATCH 16/26] Fix `web_chrome_client` --- src/android/main_pipe.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/android/main_pipe.rs b/src/android/main_pipe.rs index adb54cc2f..1b52dee07 100644 --- a/src/android/main_pipe.rs +++ b/src/android/main_pipe.rs @@ -139,12 +139,7 @@ impl<'a> MainPipe<'a> { let (activity_id, message) = CHANNEL.1.recv().unwrap(); match message { WebViewMessage::CreateWebView(attrs) => { - let Some(ActivityProxy { - activity, - webchrome_client, - .. - }) = activity_proxy(activity_id) - else { + let Some(ActivityProxy { activity, .. }) = activity_proxy(activity_id) else { #[cfg(debug_assertions)] eprintln!("no activity found for activity id: {}", activity_id); return Ok(()); @@ -290,7 +285,7 @@ impl<'a> MainPipe<'a> { &webview, "setWebChromeClient", "(Landroid/webkit/WebChromeClient;)V", - &[(&webchrome_client).into()], + &[(&web_chrome_client).into()], )?; // Add javascript interface (IPC) From 1cadf91237800d6a7b40f2e6b854a719c13f7a6d Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 11 Jun 2026 15:34:45 +0800 Subject: [PATCH 17/26] small refactor on permissionLauncher --- src/android/kotlin/WryActivity.kt | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/android/kotlin/WryActivity.kt b/src/android/kotlin/WryActivity.kt index 9fe255468..b9b97f675 100644 --- a/src/android/kotlin/WryActivity.kt +++ b/src/android/kotlin/WryActivity.kt @@ -130,23 +130,21 @@ abstract class WryActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) id = savedInstanceState?.getInt(ACTIVITY_ID_KEY) ?: intent.extras?.getInt(ACTIVITY_ID_KEY) ?: hashCode() - val permissionCallback = - ActivityResultCallback { isGranted: Map -> - permissionListener?.let { - var granted = true - for ((_, value) in isGranted) { - if (!value) granted = false - } - it(granted) - } + + permissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions(), + ) { isGranted -> + permissionListener?.let { listener -> + val allGranted = isGranted.values.all { it } + listener(allGranted) } - permissionLauncher = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions(), permissionCallback) + } activityLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result -> activityListener?.invoke(result) } + ProcessLifecycleOwner.get().lifecycle.addObserver(WryLifecycleObserver) Rust.onActivityCreate(this) } From fe9806db9f8a4e46ead7f0cecb477faa584066d2 Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 11 Jun 2026 15:39:56 +0800 Subject: [PATCH 18/26] Add `PermissionResponse::Default` docs for all platforms --- src/permissions.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/permissions.rs b/src/permissions.rs index 84363a3c7..7d9afb492 100644 --- a/src/permissions.rs +++ b/src/permissions.rs @@ -138,6 +138,7 @@ pub enum PermissionResponse { /// /// ## Platform-specific /// + /// - **Windows / macOS / Android**: The default behavior is [`Self::Prompt`] /// - **Linux**: The default behavior is [`Self::Deny`] #[default] Default, From cced03cb7644490f3de8c26e546666788b363116 Mon Sep 17 00:00:00 2001 From: F0RLE Date: Thu, 11 Jun 2026 11:32:29 +0300 Subject: [PATCH 19/26] fix(android): simplify permission JNI response --- src/android/binding.rs | 46 +++++++++-------------- src/android/kotlin/RustWebChromeClient.kt | 35 +++++------------ 2 files changed, 26 insertions(+), 55 deletions(-) diff --git a/src/android/binding.rs b/src/android/binding.rs index 516eaa9a8..520b0c2b5 100644 --- a/src/android/binding.rs +++ b/src/android/binding.rs @@ -93,7 +93,7 @@ macro_rules! android_binding { RustWebChromeClient, onPermissionRequestNative, [JString, jni::objects::JObjectArray], - jint + jboolean ); android_fn!( $domain, @@ -101,7 +101,7 @@ macro_rules! android_binding { RustWebChromeClient, onGeolocationPermissionRequestNative, [JString, JString], - jint + jboolean ); }}; } @@ -508,17 +508,15 @@ pub unsafe fn onPermissionRequestNative( _: JClass, webview_id: JString, resources: jni::objects::JObjectArray, -) -> jint { - let mut allowed = false; +) -> jboolean { let mut denied = false; - let mut prompt = false; let Ok(webview_id) = env.get_string(&webview_id) else { - return 2; + return false.into(); }; let webview_id = webview_id.to_str().ok().unwrap_or_default(); let permission_handlers = PERMISSION_HANDLER.lock().unwrap(); let Some(handler) = permission_handlers.get(webview_id) else { - return 2; + return false.into(); }; if let Ok(size) = env.get_array_length(&resources) { @@ -536,26 +534,17 @@ pub unsafe fn onPermissionRequestNative( }; match (handler.handler)(kind) { - PermissionResponse::Allow => allowed = true, PermissionResponse::Deny => denied = true, - PermissionResponse::Prompt => prompt = true, - PermissionResponse::Default => {} + PermissionResponse::Allow + | PermissionResponse::Default + | PermissionResponse::Prompt => {} } } } } } - // Consolidated decision logic - if denied { - 1 // Deny - } else if allowed { - 0 // Allow - } else if prompt { - 3 // Prompt - } else { - 2 // Default - } + denied.into() } #[allow(non_snake_case)] @@ -564,20 +553,19 @@ pub unsafe fn onGeolocationPermissionRequestNative( _: JClass, webview_id: JString, _origin: JString, -) -> jint { +) -> jboolean { let Ok(webview_id) = env.get_string(&webview_id) else { - return 2; + return false.into(); }; let webview_id = webview_id.to_str().ok().unwrap_or_default(); let permission_handlers = PERMISSION_HANDLER.lock().unwrap(); let Some(handler) = permission_handlers.get(webview_id) else { - return 2; + return false.into(); }; - match (handler.handler)(PermissionKind::Geolocation) { - PermissionResponse::Allow => 0, - PermissionResponse::Deny => 1, - PermissionResponse::Default => 2, - PermissionResponse::Prompt => 3, - } + matches!( + (handler.handler)(PermissionKind::Geolocation), + PermissionResponse::Deny + ) + .into() } diff --git a/src/android/kotlin/RustWebChromeClient.kt b/src/android/kotlin/RustWebChromeClient.kt index 416349060..a1f14c968 100644 --- a/src/android/kotlin/RustWebChromeClient.kt +++ b/src/android/kotlin/RustWebChromeClient.kt @@ -58,29 +58,14 @@ class RustWebChromeClient(private val activity: WryActivity, private val webView return } - val grantedResources = ArrayList() - val defaultResources = ArrayList() - for (resource in requestedResources) { - when (onPermissionRequestNative(webViewId, arrayOf(resource))) { - 0 -> grantedResources.add(resource) - 1 -> { - request.deny() - return - } - 2, 3 -> defaultResources.add(resource) + if (onPermissionRequestNative(webViewId, arrayOf(resource))) { + request.deny() + return } } - if (grantedResources.isNotEmpty()) { - // Android PermissionRequest can only be completed once. When the handler - // explicitly allows a subset and leaves the rest as default/prompt, grant - // only the handled subset and let the remaining resources be denied. - grantPermissionRequest(request, grantedResources.toTypedArray()) - return - } - - grantPermissionRequest(request, defaultResources.toTypedArray()) + grantPermissionRequest(request, requestedResources) } private fun grantPermissionRequest(request: PermissionRequest, resources: Array) { @@ -117,8 +102,8 @@ class RustWebChromeClient(private val activity: WryActivity, private val webView return permissionList } - private external fun onPermissionRequestNative(webviewId: String, resources: Array): Int - private external fun onGeolocationPermissionRequestNative(webviewId: String, origin: String): Int + private external fun onPermissionRequestNative(webviewId: String, resources: Array): Boolean + private external fun onGeolocationPermissionRequestNative(webviewId: String, origin: String): Boolean /** * Show the browser alert modal @@ -242,11 +227,9 @@ class RustWebChromeClient(private val activity: WryActivity, private val webView callback: GeolocationPermissions.Callback ) { super.onGeolocationPermissionsShowPrompt(origin, callback) - when (onGeolocationPermissionRequestNative(webViewId, origin)) { - 1 -> { - callback.invoke(origin, false, false) - return - } + if (onGeolocationPermissionRequestNative(webViewId, origin)) { + callback.invoke(origin, false, false) + return } Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: $origin") From 342d4f18ac361faee0a30f0c26bd29de4a298686 Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 11 Jun 2026 17:02:44 +0800 Subject: [PATCH 20/26] document allow is not supported on android --- src/permissions.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/permissions.rs b/src/permissions.rs index 7d9afb492..7a97199fe 100644 --- a/src/permissions.rs +++ b/src/permissions.rs @@ -131,6 +131,10 @@ impl std::fmt::Display for PermissionKind { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum PermissionResponse { /// Grant the permission. + /// + /// ## Platform-specific + /// + /// - **Android**: Not supported, same as [`Self::Prompt`] Allow, /// Deny the permission. Deny, From 30aa70fd7c1daa068da0b5f1128ddf5cbb9aa48c Mon Sep 17 00:00:00 2001 From: F0RLE Date: Thu, 11 Jun 2026 15:04:15 +0300 Subject: [PATCH 21/26] refactor: simplify permission responses --- src/android/binding.rs | 8 +++++--- src/android/kotlin/RustWebChromeClient.kt | 10 +++++----- src/permissions.rs | 15 +++++---------- src/webkitgtk/mod.rs | 8 ++++---- src/webview2/mod.rs | 2 +- src/wkwebview/class/wry_web_view_ui_delegate.rs | 1 - 6 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/android/binding.rs b/src/android/binding.rs index 520b0c2b5..40c27652d 100644 --- a/src/android/binding.rs +++ b/src/android/binding.rs @@ -509,6 +509,8 @@ pub unsafe fn onPermissionRequestNative( webview_id: JString, resources: jni::objects::JObjectArray, ) -> jboolean { + // Return true to deny the Android request. false lets Kotlin continue with + // Android's normal runtime permission flow. let mut denied = false; let Ok(webview_id) = env.get_string(&webview_id) else { return false.into(); @@ -535,9 +537,7 @@ pub unsafe fn onPermissionRequestNative( match (handler.handler)(kind) { PermissionResponse::Deny => denied = true, - PermissionResponse::Allow - | PermissionResponse::Default - | PermissionResponse::Prompt => {} + PermissionResponse::Allow | PermissionResponse::Default => {} } } } @@ -554,6 +554,8 @@ pub unsafe fn onGeolocationPermissionRequestNative( webview_id: JString, _origin: JString, ) -> jboolean { + // Return true to deny geolocation. false lets Kotlin continue with Android's + // normal runtime permission flow. let Ok(webview_id) = env.get_string(&webview_id) else { return false.into(); }; diff --git a/src/android/kotlin/RustWebChromeClient.kt b/src/android/kotlin/RustWebChromeClient.kt index a1f14c968..8775d1f17 100644 --- a/src/android/kotlin/RustWebChromeClient.kt +++ b/src/android/kotlin/RustWebChromeClient.kt @@ -58,11 +58,9 @@ class RustWebChromeClient(private val activity: WryActivity, private val webView return } - for (resource in requestedResources) { - if (onPermissionRequestNative(webViewId, arrayOf(resource))) { - request.deny() - return - } + if (onPermissionRequestNative(webViewId, requestedResources)) { + request.deny() + return } grantPermissionRequest(request, requestedResources) @@ -102,7 +100,9 @@ class RustWebChromeClient(private val activity: WryActivity, private val webView return permissionList } + // Returns true when Rust denies the request; false continues the normal Android permission flow. private external fun onPermissionRequestNative(webviewId: String, resources: Array): Boolean + // Returns true when Rust denies geolocation; false continues the normal Android permission flow. private external fun onGeolocationPermissionRequestNative(webviewId: String, origin: String): Boolean /** diff --git a/src/permissions.rs b/src/permissions.rs index 7a97199fe..d26ef3514 100644 --- a/src/permissions.rs +++ b/src/permissions.rs @@ -134,24 +134,20 @@ pub enum PermissionResponse { /// /// ## Platform-specific /// - /// - **Android**: Not supported, same as [`Self::Prompt`] + /// - **Android**: Not supported for runtime permissions; the normal Android + /// permission flow is used instead. Allow, /// Deny the permission. Deny, - /// Use default behavior (show system prompt). + /// Use the platform or browser default behavior. /// /// ## Platform-specific /// - /// - **Windows / macOS / Android**: The default behavior is [`Self::Prompt`] + /// - **Windows / macOS / Android**: The default behavior is to continue the + /// platform or browser permission flow. /// - **Linux**: The default behavior is [`Self::Deny`] #[default] Default, - /// Explicitly prompt the user (system dialog). - /// - /// ## Platform-specific - /// - /// - **Linux**: Not supported, same as [`Self::Deny`] - Prompt, } impl std::fmt::Display for PermissionResponse { @@ -160,7 +156,6 @@ impl std::fmt::Display for PermissionResponse { Self::Allow => write!(f, "allow"), Self::Deny => write!(f, "deny"), Self::Default => write!(f, "default"), - Self::Prompt => write!(f, "prompt"), } } } diff --git a/src/webkitgtk/mod.rs b/src/webkitgtk/mod.rs index f916be771..e1300da1f 100644 --- a/src/webkitgtk/mod.rs +++ b/src/webkitgtk/mod.rs @@ -604,7 +604,7 @@ impl InnerWebView { request.deny(); true } - PermissionResponse::Default | PermissionResponse::Prompt => false, + PermissionResponse::Default => false, }; } @@ -618,7 +618,7 @@ impl InnerWebView { match permission_handler(PermissionKind::Microphone) { PermissionResponse::Allow => {} PermissionResponse::Deny => allow = false, - PermissionResponse::Default | PermissionResponse::Prompt => handled = false, + PermissionResponse::Default => handled = false, } } @@ -627,7 +627,7 @@ impl InnerWebView { match permission_handler(PermissionKind::Camera) { PermissionResponse::Allow => {} PermissionResponse::Deny => allow = false, - PermissionResponse::Default | PermissionResponse::Prompt => handled = false, + PermissionResponse::Default => handled = false, } } @@ -661,7 +661,7 @@ impl InnerWebView { request.deny(); true } - PermissionResponse::Default | PermissionResponse::Prompt => false, + PermissionResponse::Default => false, } } }); diff --git a/src/webview2/mod.rs b/src/webview2/mod.rs index 3569fcc3c..bf87588f7 100644 --- a/src/webview2/mod.rs +++ b/src/webview2/mod.rs @@ -566,7 +566,7 @@ impl InnerWebView { PermissionResponse::Deny => { args.SetState(COREWEBVIEW2_PERMISSION_STATE_DENY)?; } - PermissionResponse::Default | PermissionResponse::Prompt => { + PermissionResponse::Default => { // Do nothing, let WebView2 show default prompt } } diff --git a/src/wkwebview/class/wry_web_view_ui_delegate.rs b/src/wkwebview/class/wry_web_view_ui_delegate.rs index 1643d51d4..7d2b0e640 100644 --- a/src/wkwebview/class/wry_web_view_ui_delegate.rs +++ b/src/wkwebview/class/wry_web_view_ui_delegate.rs @@ -139,7 +139,6 @@ define_class!( PermissionResponse::Allow => WKPermissionDecision::Grant, PermissionResponse::Deny => WKPermissionDecision::Deny, PermissionResponse::Default => WKPermissionDecision::Prompt, - PermissionResponse::Prompt => WKPermissionDecision::Prompt, }; match capture_type { From 7f8a1881bbce458ea626db24d14195df0f047c28 Mon Sep 17 00:00:00 2001 From: F0RLE Date: Thu, 11 Jun 2026 15:42:25 +0300 Subject: [PATCH 22/26] fix: update permission handler example --- examples/permission_handler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/permission_handler.rs b/examples/permission_handler.rs index 45026629a..08e5f8a05 100644 --- a/examples/permission_handler.rs +++ b/examples/permission_handler.rs @@ -26,7 +26,7 @@ fn main() -> wry::Result<()> { .with_url("https://permission.site/") .with_permission_handler(|kind| { let response = match kind { - PermissionKind::Geolocation => PermissionResponse::Prompt, + PermissionKind::Geolocation => PermissionResponse::Default, _ => PermissionResponse::Allow, }; println!("[permission] {kind} → {response}"); From dc2766cae2ea9d9005f4a88b16c4acd462a3ca44 Mon Sep 17 00:00:00 2001 From: F0RLE Date: Thu, 11 Jun 2026 16:51:00 +0300 Subject: [PATCH 23/26] fix(android): pass unknown permissions to handler --- src/android/binding.rs | 10 ++++++---- src/android/kotlin/RustWebChromeClient.kt | 5 +++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/android/binding.rs b/src/android/binding.rs index 40c27652d..e104168a8 100644 --- a/src/android/binding.rs +++ b/src/android/binding.rs @@ -502,6 +502,9 @@ pub unsafe fn onPageLoaded(mut env: JNIEnv, _: JClass, webview_id: JString, url: } } +/// Returns true to deny the Android request. +/// +/// Returns false to let Kotlin continue with Android's normal runtime permission flow. #[allow(non_snake_case)] pub unsafe fn onPermissionRequestNative( mut env: JNIEnv, @@ -509,8 +512,6 @@ pub unsafe fn onPermissionRequestNative( webview_id: JString, resources: jni::objects::JObjectArray, ) -> jboolean { - // Return true to deny the Android request. false lets Kotlin continue with - // Android's normal runtime permission flow. let mut denied = false; let Ok(webview_id) = env.get_string(&webview_id) else { return false.into(); @@ -547,6 +548,9 @@ pub unsafe fn onPermissionRequestNative( denied.into() } +/// Returns true to deny geolocation. +/// +/// Returns false to let Kotlin continue with Android's normal runtime permission flow. #[allow(non_snake_case)] pub unsafe fn onGeolocationPermissionRequestNative( mut env: JNIEnv, @@ -554,8 +558,6 @@ pub unsafe fn onGeolocationPermissionRequestNative( webview_id: JString, _origin: JString, ) -> jboolean { - // Return true to deny geolocation. false lets Kotlin continue with Android's - // normal runtime permission flow. let Ok(webview_id) = env.get_string(&webview_id) else { return false.into(); }; diff --git a/src/android/kotlin/RustWebChromeClient.kt b/src/android/kotlin/RustWebChromeClient.kt index 8775d1f17..8e19cd884 100644 --- a/src/android/kotlin/RustWebChromeClient.kt +++ b/src/android/kotlin/RustWebChromeClient.kt @@ -52,7 +52,7 @@ class RustWebChromeClient(private val activity: WryActivity, private val webView } override fun onPermissionRequest(request: PermissionRequest) { - val requestedResources = safePermissionRequestResources(request.resources) + val requestedResources = request.resources if (requestedResources.isEmpty()) { request.deny() return @@ -63,7 +63,8 @@ class RustWebChromeClient(private val activity: WryActivity, private val webView return } - grantPermissionRequest(request, requestedResources) + val grantableResources = safePermissionRequestResources(requestedResources) + grantPermissionRequest(request, grantableResources) } private fun grantPermissionRequest(request: PermissionRequest, resources: Array) { From f91f6727d54084ca9c0387e2a3daf8086b0d2cc4 Mon Sep 17 00:00:00 2001 From: F0RLE Date: Thu, 11 Jun 2026 17:02:26 +0300 Subject: [PATCH 24/26] fix(android): allow explicit unknown permissions --- src/android/binding.rs | 39 +++++++++++++++++------ src/android/kotlin/RustWebChromeClient.kt | 26 ++++++++++----- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/android/binding.rs b/src/android/binding.rs index e104168a8..b211f84fa 100644 --- a/src/android/binding.rs +++ b/src/android/binding.rs @@ -93,7 +93,7 @@ macro_rules! android_binding { RustWebChromeClient, onPermissionRequestNative, [JString, jni::objects::JObjectArray], - jboolean + jint ); android_fn!( $domain, @@ -502,27 +502,35 @@ pub unsafe fn onPageLoaded(mut env: JNIEnv, _: JClass, webview_id: JString, url: } } -/// Returns true to deny the Android request. -/// -/// Returns false to let Kotlin continue with Android's normal runtime permission flow. +const ANDROID_PERMISSION_REQUEST_DEFAULT: jint = 0; +const ANDROID_PERMISSION_REQUEST_ALLOW: jint = 1; +const ANDROID_PERMISSION_REQUEST_DENY: jint = 2; + +/// Returns allow when every requested resource was explicitly allowed, deny when +/// any resource was denied, and default otherwise. #[allow(non_snake_case)] pub unsafe fn onPermissionRequestNative( mut env: JNIEnv, _: JClass, webview_id: JString, resources: jni::objects::JObjectArray, -) -> jboolean { +) -> jint { let mut denied = false; + let mut defaulted = false; let Ok(webview_id) = env.get_string(&webview_id) else { - return false.into(); + return ANDROID_PERMISSION_REQUEST_DEFAULT; }; let webview_id = webview_id.to_str().ok().unwrap_or_default(); let permission_handlers = PERMISSION_HANDLER.lock().unwrap(); let Some(handler) = permission_handlers.get(webview_id) else { - return false.into(); + return ANDROID_PERMISSION_REQUEST_DEFAULT; }; if let Ok(size) = env.get_array_length(&resources) { + if size == 0 { + defaulted = true; + } + for i in 0..size { if let Ok(resource) = env.get_object_array_element(&resources, i) { if let Ok(resource_str) = env.get_string(&resource.into()) { @@ -538,14 +546,27 @@ pub unsafe fn onPermissionRequestNative( match (handler.handler)(kind) { PermissionResponse::Deny => denied = true, - PermissionResponse::Allow | PermissionResponse::Default => {} + PermissionResponse::Default => defaulted = true, + PermissionResponse::Allow => {} } + } else { + defaulted = true; } + } else { + defaulted = true; } } + } else { + defaulted = true; } - denied.into() + if denied { + ANDROID_PERMISSION_REQUEST_DENY + } else if defaulted { + ANDROID_PERMISSION_REQUEST_DEFAULT + } else { + ANDROID_PERMISSION_REQUEST_ALLOW + } } /// Returns true to deny geolocation. diff --git a/src/android/kotlin/RustWebChromeClient.kt b/src/android/kotlin/RustWebChromeClient.kt index 8e19cd884..b27e785e6 100644 --- a/src/android/kotlin/RustWebChromeClient.kt +++ b/src/android/kotlin/RustWebChromeClient.kt @@ -28,6 +28,11 @@ import java.text.SimpleDateFormat import java.util.* class RustWebChromeClient(private val activity: WryActivity, private val webViewId: String) : WebChromeClient() { + private companion object { + const val PERMISSION_REQUEST_DEFAULT = 0 + const val PERMISSION_REQUEST_ALLOW = 1 + const val PERMISSION_REQUEST_DENY = 2 + } /** * Render web content in `view`. @@ -58,13 +63,18 @@ class RustWebChromeClient(private val activity: WryActivity, private val webView return } - if (onPermissionRequestNative(webViewId, requestedResources)) { - request.deny() - return + when (onPermissionRequestNative(webViewId, requestedResources)) { + PERMISSION_REQUEST_DENY -> request.deny() + PERMISSION_REQUEST_ALLOW -> grantPermissionRequest(request, requestedResources) + PERMISSION_REQUEST_DEFAULT -> { + val grantableResources = safePermissionRequestResources(requestedResources) + grantPermissionRequest(request, grantableResources) + } + else -> { + val grantableResources = safePermissionRequestResources(requestedResources) + grantPermissionRequest(request, grantableResources) + } } - - val grantableResources = safePermissionRequestResources(requestedResources) - grantPermissionRequest(request, grantableResources) } private fun grantPermissionRequest(request: PermissionRequest, resources: Array) { @@ -101,8 +111,8 @@ class RustWebChromeClient(private val activity: WryActivity, private val webView return permissionList } - // Returns true when Rust denies the request; false continues the normal Android permission flow. - private external fun onPermissionRequestNative(webviewId: String, resources: Array): Boolean + // Returns one of the PERMISSION_REQUEST_* constants. + private external fun onPermissionRequestNative(webviewId: String, resources: Array): Int // Returns true when Rust denies geolocation; false continues the normal Android permission flow. private external fun onGeolocationPermissionRequestNative(webviewId: String, origin: String): Boolean From 373ca8ae977a169b2021a780a7f6af60f1182c7a Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 11 Jun 2026 22:28:45 +0800 Subject: [PATCH 25/26] onPermissionRequestNative will not return other values --- src/android/kotlin/RustWebChromeClient.kt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/android/kotlin/RustWebChromeClient.kt b/src/android/kotlin/RustWebChromeClient.kt index b27e785e6..7b9806518 100644 --- a/src/android/kotlin/RustWebChromeClient.kt +++ b/src/android/kotlin/RustWebChromeClient.kt @@ -67,12 +67,7 @@ class RustWebChromeClient(private val activity: WryActivity, private val webView PERMISSION_REQUEST_DENY -> request.deny() PERMISSION_REQUEST_ALLOW -> grantPermissionRequest(request, requestedResources) PERMISSION_REQUEST_DEFAULT -> { - val grantableResources = safePermissionRequestResources(requestedResources) - grantPermissionRequest(request, grantableResources) - } - else -> { - val grantableResources = safePermissionRequestResources(requestedResources) - grantPermissionRequest(request, grantableResources) + grantPermissionRequest(request, filterKnownPermissions(requestedResources)) } } } @@ -273,7 +268,7 @@ class RustWebChromeClient(private val activity: WryActivity, private val webView } } - private fun safePermissionRequestResources(resources: Array): Array { + private fun filterKnownPermissions(resources: Array): Array { return resources.filter { it == PermissionRequest.RESOURCE_AUDIO_CAPTURE || it == PermissionRequest.RESOURCE_VIDEO_CAPTURE || From e7c84dc2ee460933a4f61bab3c9f1b2d3d33e337 Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 11 Jun 2026 22:56:28 +0800 Subject: [PATCH 26/26] Send individual permissions to rust side and support partial allow --- src/android/binding.rs | 64 +++++++---------------- src/android/kotlin/RustWebChromeClient.kt | 29 ++++++---- 2 files changed, 39 insertions(+), 54 deletions(-) diff --git a/src/android/binding.rs b/src/android/binding.rs index b211f84fa..c34634a95 100644 --- a/src/android/binding.rs +++ b/src/android/binding.rs @@ -92,7 +92,7 @@ macro_rules! android_binding { $package, RustWebChromeClient, onPermissionRequestNative, - [JString, jni::objects::JObjectArray], + [JString, JString], jint ); android_fn!( @@ -506,17 +506,14 @@ const ANDROID_PERMISSION_REQUEST_DEFAULT: jint = 0; const ANDROID_PERMISSION_REQUEST_ALLOW: jint = 1; const ANDROID_PERMISSION_REQUEST_DENY: jint = 2; -/// Returns allow when every requested resource was explicitly allowed, deny when -/// any resource was denied, and default otherwise. +/// Returns `ANDROID_PERMISSION_REQUEST_DEFAULT | ANDROID_PERMISSION_REQUEST_ALLOW | ANDROID_PERMISSION_REQUEST_DENY` #[allow(non_snake_case)] pub unsafe fn onPermissionRequestNative( mut env: JNIEnv, _: JClass, webview_id: JString, - resources: jni::objects::JObjectArray, + resource: JString, ) -> jint { - let mut denied = false; - let mut defaulted = false; let Ok(webview_id) = env.get_string(&webview_id) else { return ANDROID_PERMISSION_REQUEST_DEFAULT; }; @@ -526,46 +523,23 @@ pub unsafe fn onPermissionRequestNative( return ANDROID_PERMISSION_REQUEST_DEFAULT; }; - if let Ok(size) = env.get_array_length(&resources) { - if size == 0 { - defaulted = true; - } - - for i in 0..size { - if let Ok(resource) = env.get_object_array_element(&resources, i) { - if let Ok(resource_str) = env.get_string(&resource.into()) { - let resource_str = resource_str.to_string_lossy(); - - let kind = match resource_str.as_ref() { - "android.webkit.resource.AUDIO_CAPTURE" => PermissionKind::Microphone, - "android.webkit.resource.VIDEO_CAPTURE" => PermissionKind::Camera, - "android.webkit.resource.PROTECTED_MEDIA_ID" => PermissionKind::MediaKeySystemAccess, - "android.webkit.resource.MIDI_SYSEX" => PermissionKind::Midi, - _ => PermissionKind::Other, - }; - - match (handler.handler)(kind) { - PermissionResponse::Deny => denied = true, - PermissionResponse::Default => defaulted = true, - PermissionResponse::Allow => {} - } - } else { - defaulted = true; - } - } else { - defaulted = true; - } - } - } else { - defaulted = true; - } + let Ok(resource_str) = env.get_string(&resource) else { + return ANDROID_PERMISSION_REQUEST_DEFAULT; + }; + let resource_str = resource_str.to_string_lossy(); + + let kind = match resource_str.as_ref() { + "android.webkit.resource.AUDIO_CAPTURE" => PermissionKind::Microphone, + "android.webkit.resource.VIDEO_CAPTURE" => PermissionKind::Camera, + "android.webkit.resource.PROTECTED_MEDIA_ID" => PermissionKind::MediaKeySystemAccess, + "android.webkit.resource.MIDI_SYSEX" => PermissionKind::Midi, + _ => PermissionKind::Other, + }; - if denied { - ANDROID_PERMISSION_REQUEST_DENY - } else if defaulted { - ANDROID_PERMISSION_REQUEST_DEFAULT - } else { - ANDROID_PERMISSION_REQUEST_ALLOW + match (handler.handler)(kind) { + PermissionResponse::Default => ANDROID_PERMISSION_REQUEST_DEFAULT, + PermissionResponse::Allow => ANDROID_PERMISSION_REQUEST_ALLOW, + PermissionResponse::Deny => ANDROID_PERMISSION_REQUEST_DENY, } } diff --git a/src/android/kotlin/RustWebChromeClient.kt b/src/android/kotlin/RustWebChromeClient.kt index 7b9806518..70c97a794 100644 --- a/src/android/kotlin/RustWebChromeClient.kt +++ b/src/android/kotlin/RustWebChromeClient.kt @@ -63,13 +63,20 @@ class RustWebChromeClient(private val activity: WryActivity, private val webView return } - when (onPermissionRequestNative(webViewId, requestedResources)) { - PERMISSION_REQUEST_DENY -> request.deny() - PERMISSION_REQUEST_ALLOW -> grantPermissionRequest(request, requestedResources) - PERMISSION_REQUEST_DEFAULT -> { - grantPermissionRequest(request, filterKnownPermissions(requestedResources)) + val allowedResources = ArrayList() + val defaultResources = ArrayList() + + for (resource in requestedResources) { + when (onPermissionRequestNative(webViewId, resource)) { + PERMISSION_REQUEST_DENY -> {} + PERMISSION_REQUEST_ALLOW -> allowedResources.add(resource) + PERMISSION_REQUEST_DEFAULT -> defaultResources.add(resource) } } + + val resources = + allowedResources.plus(filterKnownPermissions(defaultResources)).toList().toTypedArray() + grantPermissionRequest(request, resources) } private fun grantPermissionRequest(request: PermissionRequest, resources: Array) { @@ -106,9 +113,13 @@ class RustWebChromeClient(private val activity: WryActivity, private val webView return permissionList } - // Returns one of the PERMISSION_REQUEST_* constants. - private external fun onPermissionRequestNative(webviewId: String, resources: Array): Int - // Returns true when Rust denies geolocation; false continues the normal Android permission flow. + /** + * @return one of the PERMISSION_REQUEST_* constants. + */ + private external fun onPermissionRequestNative(webviewId: String, resource: String): Int + /** + * @return true when Rust denies geolocation; false continues the normal Android permission flow. + */ private external fun onGeolocationPermissionRequestNative(webviewId: String, origin: String): Boolean /** @@ -268,7 +279,7 @@ class RustWebChromeClient(private val activity: WryActivity, private val webView } } - private fun filterKnownPermissions(resources: Array): Array { + private fun filterKnownPermissions(resources: List): Array { return resources.filter { it == PermissionRequest.RESOURCE_AUDIO_CAPTURE || it == PermissionRequest.RESOURCE_VIDEO_CAPTURE ||