Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changes/permission-handler.md
Original file line number Diff line number Diff line change
@@ -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.
47 changes: 47 additions & 0 deletions examples/permission_handler.rs
Original file line number Diff line number Diff line change
@@ -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;
}
});
}
62 changes: 59 additions & 3 deletions src/android/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -97,6 +97,14 @@ macro_rules! android_binding {
handleReceivedTitle,
[JObject, JString],
);
android_fn!(
$domain,
$package,
RustWebChromeClient,
onPermissionRequestNative,
[jni::objects::JObjectArray],
jint
);
}};
}

Expand Down Expand Up @@ -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
}
}
33 changes: 28 additions & 5 deletions src/android/kotlin/RustWebChromeClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = ArrayList()
if (listOf(*request.resources).contains("android.webkit.resource.VIDEO_CAPTURE")) {
Expand All @@ -118,6 +136,8 @@ class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() {
}
}

private external fun onPermissionRequestNative(resources: Array<String>): Int

/**
* Show the browser alert modal
* @param view
Expand Down Expand Up @@ -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<String>): Int
private external fun handleReceivedTitle(webview: WebView, title: String)
}
5 changes: 4 additions & 1 deletion src/android/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -81,6 +83,7 @@ define_static_handlers! {
TITLE_CHANGE_HANDLER = UnsafeTitleHandler { handler: Box<dyn Fn(String)> };
URL_LOADING_OVERRIDE = UnsafeUrlLoadingOverride { handler: Box<dyn Fn(String) -> bool> };
ON_LOAD_HANDLER = UnsafeOnPageLoadHandler { handler: Box<dyn Fn(PageLoadEvent, String)> };
PERMISSION_HANDLER = UnsafePermissionHandler { handler: Box<dyn Fn(PermissionKind) -> PermissionResponse> };
}

pub static WITH_ASSET_LOADER: StaticValue<Option<bool>> = StaticValue(Mutex::new(None));
Expand Down
2 changes: 1 addition & 1 deletion src/custom_protocol_workaround.rs
Original file line number Diff line number Diff line change
@@ -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 <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:
Expand Down
Loading
Loading