Skip to content

Commit 112bbeb

Browse files
committed
feat: expand permission handling API and add Android support
Signed-off-by: F0RLE <lrshka.klim7766@gmail.com>
1 parent b15fc6a commit 112bbeb

8 files changed

Lines changed: 171 additions & 52 deletions

File tree

.changes/permission-handler.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@
22
"wry": minor
33
---
44

5-
feat: add permission handler API for WebView2
5+
Add an expanded permission handling API for WebView2, WKWebView, and WebKitGTK.
6+
This includes:
7+
- `PermissionKind` expansion (Midi, Nfc, Bluetooth, Sensors, MediaKeySystemAccess, LocalFonts, WindowManagement).
8+
- Support for `PermissionResponse::Prompt`.
9+
- Android support (experimental) via JNI.
10+
- Improved macOS permission splitting for camera and microphone.

src/android/binding.rs

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ pub use jni::{
1616
pub use ndk;
1717

1818
use super::{
19-
ASSET_LOADER_DOMAIN, EVAL_CALLBACKS, IPC, ON_LOAD_HANDLER, REQUEST_HANDLER, TITLE_CHANGE_HANDLER,
20-
URL_LOADING_OVERRIDE, WITH_ASSET_LOADER,
19+
ASSET_LOADER_DOMAIN, EVAL_CALLBACKS, IPC, ON_LOAD_HANDLER, PERMISSION_HANDLER, REQUEST_HANDLER,
20+
TITLE_CHANGE_HANDLER, URL_LOADING_OVERRIDE, WITH_ASSET_LOADER,
2121
};
2222

23-
use crate::PageLoadEvent;
23+
use crate::{PageLoadEvent, PermissionKind, PermissionResponse};
2424

2525
#[macro_export]
2626
macro_rules! android_binding {
@@ -97,6 +97,14 @@ macro_rules! android_binding {
9797
handleReceivedTitle,
9898
[JObject, JString],
9999
);
100+
android_fn!(
101+
$domain,
102+
$package,
103+
RustWebChromeClient,
104+
onPermissionRequestNative,
105+
[jni::objects::JObjectArray],
106+
jint
107+
);
100108
}};
101109
}
102110

@@ -413,3 +421,53 @@ pub unsafe fn onPageLoaded(mut env: JNIEnv, _: JClass, url: JString) {
413421
}
414422
}
415423
}
424+
425+
pub unsafe fn onPermissionRequestNative(
426+
mut env: JNIEnv,
427+
_: JClass,
428+
resources: jni::objects::JObjectArray,
429+
) -> jint {
430+
let mut kind = PermissionKind::Other;
431+
432+
if let Ok(size) = env.get_array_length(&resources) {
433+
for i in 0..size {
434+
if let Ok(resource) = env.get_object_array_element(&resources, i) {
435+
if let Ok(resource_str) = env.get_string(&resource.into()) {
436+
let resource_str = resource_str.to_string_lossy();
437+
438+
match resource_str.as_ref() {
439+
"android.webkit.resource.AUDIO_CAPTURE" => {
440+
kind = PermissionKind::Microphone;
441+
break;
442+
}
443+
"android.webkit.resource.VIDEO_CAPTURE" => {
444+
kind = PermissionKind::Camera;
445+
break;
446+
}
447+
"android.webkit.resource.PROTECTED_MEDIA_ID" => {
448+
kind = PermissionKind::MediaKeySystemAccess;
449+
break;
450+
}
451+
"android.webkit.resource.MIDI_SYSEX" => {
452+
kind = PermissionKind::Midi;
453+
break;
454+
}
455+
_ => {}
456+
}
457+
}
458+
}
459+
}
460+
}
461+
462+
if let Some(handler) = &*PERMISSION_HANDLER.lock().unwrap() {
463+
let response = (handler.handler)(kind);
464+
match response {
465+
PermissionResponse::Allow => 0,
466+
PermissionResponse::Deny => 1,
467+
PermissionResponse::Default => 2,
468+
PermissionResponse::Prompt => 3,
469+
}
470+
} else {
471+
2 // Default
472+
}
473+
}

src/android/kotlin/RustWebChromeClient.kt

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,24 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() {
9292
}
9393

9494
override fun onPermissionRequest(request: PermissionRequest) {
95+
val response = onPermissionRequestNative(request.resources)
96+
when (response) {
97+
0 -> { // Allow
98+
request.grant(request.resources)
99+
return
100+
}
101+
1 -> { // Deny
102+
request.deny()
103+
return
104+
}
105+
2 -> { // Default
106+
// Continue with default logic
107+
}
108+
3 -> { // Prompt
109+
// Continue with default logic (which prompts)
110+
}
111+
}
112+
95113
val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
96114
val permissionList: MutableList<String> = ArrayList()
97115
if (listOf(*request.resources).contains("android.webkit.resource.VIDEO_CAPTURE")) {
@@ -118,6 +136,8 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() {
118136
}
119137
}
120138

139+
private external fun onPermissionRequestNative(resources: Array<String>): Int
140+
121141
/**
122142
* Show the browser alert modal
123143
* @param view
@@ -482,12 +502,15 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() {
482502
return File.createTempFile(imageFileName, ".jpg", storageDir)
483503
}
484504

485-
override fun onReceivedTitle(
486-
view: WebView,
487-
title: String
488-
) {
489-
handleReceivedTitle(view, title)
505+
override fun onPermissionRequest(request: PermissionRequest) {
506+
val result = onPermissionRequestNative(request.resources)
507+
when (result) {
508+
0 -> request.grant(request.resources)
509+
1 -> request.deny()
510+
else -> super.onPermissionRequest(request)
511+
}
490512
}
491513

514+
private external fun onPermissionRequestNative(resources: Array<String>): Int
492515
private external fun handleReceivedTitle(webview: WebView, title: String)
493516
}

src/android/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ define_static_handlers! {
8181
TITLE_CHANGE_HANDLER = UnsafeTitleHandler { handler: Box<dyn Fn(String)> };
8282
URL_LOADING_OVERRIDE = UnsafeUrlLoadingOverride { handler: Box<dyn Fn(String) -> bool> };
8383
ON_LOAD_HANDLER = UnsafeOnPageLoadHandler { handler: Box<dyn Fn(PageLoadEvent, String)> };
84+
PERMISSION_HANDLER = UnsafePermissionHandler { handler: Box<dyn Fn(PermissionKind) -> PermissionResponse> };
8485
}
8586

8687
pub static WITH_ASSET_LOADER: StaticValue<Option<bool>> = StaticValue(Mutex::new(None));

src/lib.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,20 @@ pub enum PermissionKind {
553553
ClipboardRead,
554554
/// Display capture permission (for getDisplayMedia).
555555
DisplayCapture,
556+
/// Midi access permission.
557+
Midi,
558+
/// Nfc access permission.
559+
Nfc,
560+
/// Bluetooth access permission.
561+
Bluetooth,
562+
/// Sensors (accelerometer, gyroscope, etc.) access permission.
563+
Sensors,
564+
/// Media key system access permission.
565+
MediaKeySystemAccess,
566+
/// Local fonts access permission.
567+
LocalFonts,
568+
/// Window management permission.
569+
WindowManagement,
556570
/// Other unrecognized permission type.
557571
Other,
558572
}
@@ -839,12 +853,17 @@ pub struct WebViewAttributes<'a> {
839853
/// The handler receives the [`PermissionKind`] and should return
840854
/// the desired [`PermissionResponse`].
841855
///
856+
/// > [!NOTE]
857+
/// > This handler only triggers for new permission requests. If the user has already
858+
/// > allowed or denied a permission persistently within the webview, the browser
859+
/// > will use the saved preference instead of calling this handler.
860+
///
842861
/// ## Platform-specific:
843862
///
844863
/// - **Windows**: Fully supported via WebView2's PermissionRequested event.
845864
/// - **macOS / iOS**: Fully supported via WKUIDelegate's requestMediaCapturePermission.
846865
/// - **Linux**: Fully supported via WebKitGTK's permission-request signal.
847-
/// - **Android**: Not yet implemented, handler is ignored.
866+
/// - **Android**: Experimental support implemented via JNI.
848867
///
849868
/// ## Example
850869
///

src/webkitgtk/mod.rs

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -578,28 +578,27 @@ impl InnerWebView {
578578

579579
// Permission handler
580580
if let Some(permission_handler) = attributes.permission_handler.take() {
581-
let permission_handler = Rc::new(permission_handler);
582581
webview.connect_permission_request(move |_webview, request| {
583582
// Determine permission kind
584-
let permission_kind = if request
585-
.downcast_ref::<UserMediaPermissionRequest>()
586-
.is_some()
587-
{
588-
let media_request = request
589-
.downcast_ref::<UserMediaPermissionRequest>()
590-
.unwrap();
591-
if media_request.is_for_audio_device() {
592-
PermissionKind::Microphone
593-
} else if media_request.is_for_video_device() {
594-
PermissionKind::Camera
583+
let permission_kind =
584+
if let Some(media_request) = request.downcast_ref::<UserMediaPermissionRequest>() {
585+
if media_request.is_for_audio_device() {
586+
PermissionKind::Microphone
587+
} else if media_request.is_for_video_device() {
588+
PermissionKind::Camera
589+
} else {
590+
// TODO: Use is_for_display_device() when we bump to WebKitGTK 2.42
591+
PermissionKind::DisplayCapture
592+
}
593+
} else if request.type_().name() == "WebKitGeolocationPermissionRequest" {
594+
PermissionKind::Geolocation
595+
} else if request.type_().name() == "WebKitNotificationPermissionRequest" {
596+
PermissionKind::Notifications
597+
} else if request.type_().name() == "WebKitPointerLockPermissionRequest" {
598+
PermissionKind::Other // Pointer lock not yet in PermissionKind
595599
} else {
596-
// TODO: Use is_for_display_device() when we bump to WebKitGTK 2.42
597-
PermissionKind::DisplayCapture
598-
}
599-
} else {
600-
// Could be GeolocationPermissionRequest, NotificationPermissionRequest, etc.
601-
PermissionKind::Other
602-
};
600+
PermissionKind::Other
601+
};
603602

604603
// Call user's permission handler
605604
let response = permission_handler(permission_kind);
@@ -614,10 +613,7 @@ impl InnerWebView {
614613
request.deny();
615614
true // handled
616615
}
617-
PermissionResponse::Default => {
618-
false // not handled, let WebKitGTK show default prompt
619-
}
620-
PermissionResponse::Prompt => {
616+
PermissionResponse::Default | PermissionResponse::Prompt => {
621617
false // not handled, let WebKitGTK show default prompt
622618
}
623619
}

src/webview2/mod.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,6 @@ impl InnerWebView {
511511

512512
// Permission handler
513513
if let Some(permission_handler) = attributes.permission_handler.take() {
514-
let permission_handler = Rc::new(permission_handler);
515514
unsafe {
516515
webview.add_PermissionRequested(
517516
&PermissionRequestedEventHandler::create(Box::new(move |_, args| {
@@ -527,6 +526,7 @@ impl InnerWebView {
527526
COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION => PermissionKind::Geolocation,
528527
COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS => PermissionKind::Notifications,
529528
COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ => PermissionKind::ClipboardRead,
529+
COREWEBVIEW2_PERMISSION_KIND_LOCAL_FONTS => PermissionKind::LocalFonts,
530530
_ => PermissionKind::Other,
531531
};
532532

@@ -541,10 +541,7 @@ impl InnerWebView {
541541
PermissionResponse::Deny => {
542542
args.SetState(COREWEBVIEW2_PERMISSION_STATE_DENY)?;
543543
}
544-
PermissionResponse::Default => {
545-
// Do nothing, let WebView2 show default prompt
546-
}
547-
PermissionResponse::Prompt => {
544+
PermissionResponse::Default | PermissionResponse::Prompt => {
548545
// Do nothing, let WebView2 show default prompt
549546
}
550547
}

src/wkwebview/class/wry_web_view_ui_delegate.rs

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -136,24 +136,44 @@ define_class!(
136136
capture_type: WKMediaCaptureType,
137137
decision_handler: &Block<dyn Fn(WKPermissionDecision)>,
138138
) {
139-
// Determine permission kind based on capture type
140-
let permission_kind = match capture_type {
141-
WKMediaCaptureType::Camera => PermissionKind::Camera,
142-
WKMediaCaptureType::Microphone => PermissionKind::Microphone,
143-
WKMediaCaptureType::CameraAndMicrophone => PermissionKind::Microphone, // Treat as microphone for now
144-
_ => PermissionKind::Other,
145-
};
146-
147139
// Call user's permission handler if set
148140
let decision = if let Some(handler) = &self.ivars().permission_handler {
149-
match handler(permission_kind) {
150-
PermissionResponse::Allow => WKPermissionDecision::Grant,
151-
PermissionResponse::Deny => WKPermissionDecision::Deny,
152-
PermissionResponse::Default => WKPermissionDecision::Grant, // Default to grant for backwards compatibility
153-
PermissionResponse::Prompt => WKPermissionDecision::Prompt,
141+
match capture_type {
142+
WKMediaCaptureType::Camera => match handler(PermissionKind::Camera) {
143+
PermissionResponse::Allow => WKPermissionDecision::Grant,
144+
PermissionResponse::Deny => WKPermissionDecision::Deny,
145+
PermissionResponse::Default => WKPermissionDecision::Grant,
146+
PermissionResponse::Prompt => WKPermissionDecision::Prompt,
147+
},
148+
WKMediaCaptureType::Microphone => match handler(PermissionKind::Microphone) {
149+
PermissionResponse::Allow => WKPermissionDecision::Grant,
150+
PermissionResponse::Deny => WKPermissionDecision::Deny,
151+
PermissionResponse::Default => WKPermissionDecision::Grant,
152+
PermissionResponse::Prompt => WKPermissionDecision::Prompt,
153+
},
154+
WKMediaCaptureType::CameraAndMicrophone => {
155+
let mic_res = handler(PermissionKind::Microphone);
156+
let cam_res = handler(PermissionKind::Camera);
157+
158+
match (mic_res, cam_res) {
159+
(PermissionResponse::Allow, PermissionResponse::Allow) => WKPermissionDecision::Grant,
160+
(PermissionResponse::Deny, _) | (_, PermissionResponse::Deny) => {
161+
WKPermissionDecision::Deny
162+
}
163+
(PermissionResponse::Prompt, _) | (_, PermissionResponse::Prompt) => {
164+
WKPermissionDecision::Prompt
165+
}
166+
_ => WKPermissionDecision::Grant,
167+
}
168+
}
169+
_ => match handler(PermissionKind::Other) {
170+
PermissionResponse::Allow => WKPermissionDecision::Grant,
171+
PermissionResponse::Deny => WKPermissionDecision::Deny,
172+
PermissionResponse::Default => WKPermissionDecision::Grant,
173+
PermissionResponse::Prompt => WKPermissionDecision::Prompt,
174+
},
154175
}
155176
} else {
156-
// No handler set, default to grant (backwards compatible behavior)
157177
WKPermissionDecision::Grant
158178
};
159179

0 commit comments

Comments
 (0)