feat: 大屏桌面壳(Tauri 2)

This commit is contained in:
2026-08-23 22:34:50 +08:00
parent 28e233520b
commit 9700eb652b
123 changed files with 26695 additions and 0 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

+7
View File
@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
+5145
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "昆明大学生创业园展播系统"
version = "0.1.2"
description = "大屏媒体轮播系统 - Tauri App"
authors = ["Pine"]
edition = "2021"
[lib]
name = "dpm_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
tauri-plugin-autostart = "2"
serde = { version = "1", features = ["derive"] }
# 本地补丁:wry 0.55.1 的 platform_webview_version 在 macOS 26 上
# NSBundle::bundleWithIdentifier 触发 CFRelease 断言崩溃(SIGTRAP),
# 已改为安全占位实现(见 vendor/wry/src/wkwebview/mod.rs)
[patch.crates-io]
wry = { path = "vendor/wry" }
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- 摄像头:大屏前的实时识别(人脸/姿态检测、手势控制)。声明后 macOS 仅首次授权一次 -->
<key>NSCameraUsageDescription</key>
<string>用于大屏前的摄像头实时识别(人脸检测、手势控制),画面仅在本机处理,不对外传输。</string>
<!-- 麦克风:语音对话(实时语音识别与合成) -->
<key>NSMicrophoneUsageDescription</key>
<string>用于实时语音对话(语音识别与合成),音频仅在本机处理。</string>
</dict>
</plist>
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
+15
View File
@@ -0,0 +1,15 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main", "secondary"],
"permissions": [
"core:default",
"core:app:default",
"core:window:default",
"core:window:allow-minimize",
"core:window:allow-set-fullscreen",
"opener:default",
"autostart:default"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

+151
View File
@@ -0,0 +1,151 @@
// 说明:早期 Rust 内嵌后端(server/storage/models/events,axum :10801)已随架构迁移到
// Python 后端(FastAPI :10085)而移除;当前 Rust 侧仅负责窗口与生命周期,
// 外加一个「运行时配置读取」命令(供前端读取 exe 旁 config.json,部署免重打包改地址)。
use std::fs;
use std::path::PathBuf;
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
/// 双屏控制:on=true 在第二块屏幕上新建一个全屏窗口(加载前端首页),on=false 关闭它。
/// 用于 admin 端「启动双屏」,让展播内容同时投到电脑的另一块显示器。
#[tauri::command]
fn set_dual_screen(app: AppHandle, on: bool, parent_id: Option<String>) -> Result<String, String> {
if !on {
if let Some(w) = app.get_webview_window("secondary") {
let _ = w.close();
}
return Ok("off".into());
}
// 已存在则直接显示/全屏/聚焦
if let Some(w) = app.get_webview_window("secondary") {
let _ = w.show();
let _ = w.unminimize();
let _ = w.set_fullscreen(true);
let _ = w.set_focus();
return Ok("on".into());
}
// 主屏位置(用于排除主屏;必须把副窗放到主屏之外)
let primary_pos = app
.primary_monitor()
.ok()
.flatten()
.map(|pm| { let p = pm.position(); (p.x, p.y) });
// 获取全部显示器,寻找一块与主屏位置不同的副屏
let monitors = app.available_monitors().map_err(|e| format!("读取显示器失败: {e}"))?;
eprintln!("[dual] monitors={} primary_pos={:?}", monitors.len(), primary_pos);
let mut target: Option<(i32, i32, u32, u32)> = None; // (x, y, w, h)
for m in &monitors {
let p = m.position();
if Some((p.x, p.y)) != primary_pos {
let s = m.size();
target = Some((p.x, p.y, s.width, s.height));
break;
}
}
// 绝对不允许在主屏桌面上开启;没有副屏则拒绝
let (x, y, w, h) = match target {
Some(t) => t,
None => return Err("未检测到副屏(需连接第二块显示器),无法开启双屏".into()),
};
eprintln!("[dual] secondary {}x{} @ {},{}", w, h, x, y);
let win = WebviewWindowBuilder::new(&app, "secondary", WebviewUrl::default())
.title("OPC 运营中心 · 双屏")
.position(x as f64, y as f64)
.inner_size(w as f64, h as f64)
// 由 Rust 明确告知副窗:它的身份是 secondary,且从属于主屏 device_id(parent_id)
.initialization_script(format!(
"window.__DPM_WINDOW_LABEL__='secondary';window.__DPM_PARENT_ID__='{}';",
parent_id.unwrap_or_default().replace('\'', "")
))
.build()
.map_err(|e| format!("创建副窗失败: {e}"))?;
// 等窗口内容就绪后再全屏(避免过早全屏导致白屏/布局异常)
let win2 = win.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(400));
let _ = win2.set_fullscreen(true);
let _ = win2.set_focus();
});
Ok("on".into())
}
/// 安装更新:把前端已下载的安装包字节写入磁盘 → 启动安装程序 → 退出应用。
/// bytes 由前端 fetch /download 得到;启动安装程序后进程独立,应用退出不影响它。
#[tauri::command]
fn install_update(app: AppHandle, bytes: Vec<u8>, filename: String) -> Result<String, String> {
if bytes.len() < 1000 {
return Err("下载内容无效或为空".into());
}
let dest = std::env::temp_dir().join(filename);
std::fs::write(&dest, &bytes).map_err(|e| format!("写入安装包失败: {e}"))?;
// 启动安装程序(Windows 下 Command::spawn 为独立进程,应用退出后仍在运行)
std::process::Command::new(&dest)
.spawn()
.map_err(|e| format!("启动安装程序失败: {e}"))?;
// 稍后退出应用,让安装程序接管
let app2 = app.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(800));
app2.exit(0);
});
Ok(dest.to_string_lossy().into_owned())
}
/// 读取运行时配置(部署用,免重新打包):
/// 1) exe 同目录 config.json / dpm.config.json(最高优先)
/// 2) 应用配置目录 config.json
/// 内容示例:
/// { "api_base": "http://192.168.1.9:10085",
/// "mqtt_url": "ws://192.168.1.3:8083/mqtt",
/// "mqtt_username": "dpm", "mqtt_password": "123456" }
/// 返回 JSON 字符串;未配置返回 null。
#[tauri::command]
fn read_runtime_config(app: AppHandle) -> Option<String> {
let mut candidates: Vec<PathBuf> = Vec::new();
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
candidates.push(dir.join("config.json"));
candidates.push(dir.join("dpm.config.json"));
}
}
if let Ok(dir) = app.path().app_config_dir() {
candidates.push(dir.join("config.json"));
}
for p in candidates {
if p.exists() {
if let Ok(s) = fs::read_to_string(&p) {
let t = s.trim();
if t.starts_with('{') {
return Some(t.to_string());
}
}
}
}
None
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
Some(vec![]),
))
.invoke_handler(tauri::generate_handler![
read_runtime_config,
set_dual_screen,
install_update
])
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
// 仅主窗口关闭=最小化(进程常驻);双屏副窗口可正常关闭
if window.label() == "main" {
api.prevent_close();
let _ = window.minimize();
}
}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+6
View File
@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
dpm_lib::run()
}
+40
View File
@@ -0,0 +1,40 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "云超服昆创园OPC运营中心",
"version": "0.1.2",
"identifier": "com.pine.dpm",
"build": {
"beforeDevCommand": "yarn build && yarn dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "yarn build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "云超服昆创园OPC运营中心",
"width": 1200,
"height": 800,
"fullscreen": true,
"additionalBrowserArgs": "--autoplay-policy=no-user-gesture-required --enable-features=WebRtcHideLocalIpsWithMdns"
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": ["nsis", "app", "dmg"],
"macOS": {
"minimumSystemVersion": "10.15"
},
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
+1
View File
@@ -0,0 +1 @@
{"v":1}
+4
View File
@@ -0,0 +1,4 @@
# [build]
# target = "aarch64-linux-android"
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-arg=-mmacosx-version-min=10.12"]
+9
View File
@@ -0,0 +1,9 @@
# Copyright 2020-2023 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
target
gh-pages
.DS_Store
examples/test_video.mp4
.vscode/
+3
View File
@@ -0,0 +1,3 @@
// Copyright {20\d{2}(-20\d{2})?} Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
+1270
View File
File diff suppressed because it is too large Load Diff
+4354
View File
File diff suppressed because it is too large Load Diff
+424
View File
@@ -0,0 +1,424 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.77"
name = "wry"
version = "0.55.1"
authors = ["Tauri Programme within The Commons Conservancy"]
build = "build.rs"
exclude = [
"/.changes",
"/.github",
"/audits",
"/wry-logo.svg",
]
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Cross-platform WebView rendering library"
documentation = "https://docs.rs/wry"
readme = "README.md"
categories = ["gui"]
license = "Apache-2.0 OR MIT"
repository = "https://github.com/tauri-apps/wry"
[package.metadata.docs.rs]
no-default-features = true
features = [
"protocol",
"os-webview",
]
targets = [
"x86_64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
"x86_64-apple-darwin",
]
[features]
default = [
"protocol",
"os-webview",
"x11",
]
devtools = []
fullscreen = []
linux-body = [
"webkit2gtk/v2_40",
"os-webview",
]
mac-proxy = []
os-webview = [
"javascriptcore-rs",
"webkit2gtk",
"webkit2gtk-sys",
"dep:gtk",
"soup3",
]
protocol = []
serde = ["dpi/serde"]
tracing = ["dep:tracing"]
transparent = []
x11 = [
"x11-dl",
"gdkx11",
"tao/x11",
]
[lib]
name = "wry"
path = "src/lib.rs"
[[example]]
name = "async_custom_protocol"
path = "examples/async_custom_protocol.rs"
[[example]]
name = "cookies"
path = "examples/cookies.rs"
[[example]]
name = "custom_protocol"
path = "examples/custom_protocol.rs"
[[example]]
name = "custom_titlebar"
path = "examples/custom_titlebar.rs"
[[example]]
name = "gtk_multiwebview"
path = "examples/gtk_multiwebview.rs"
[[example]]
name = "gtk_opengl"
path = "examples/gtk_opengl.rs"
[[example]]
name = "multiwebview"
path = "examples/multiwebview.rs"
[[example]]
name = "multiwindow"
path = "examples/multiwindow.rs"
[[example]]
name = "reparent"
path = "examples/reparent.rs"
[[example]]
name = "simple"
path = "examples/simple.rs"
[[example]]
name = "streaming"
path = "examples/streaming.rs"
[[example]]
name = "transparent"
path = "examples/transparent.rs"
[[example]]
name = "wgpu"
path = "examples/wgpu.rs"
[[example]]
name = "window_border"
path = "examples/window_border.rs"
[[example]]
name = "winit"
path = "examples/winit.rs"
[dependencies.cookie]
version = "0.18"
[dependencies.dpi]
version = "0.1"
[dependencies.http]
version = "1.1"
[dependencies.once_cell]
version = "1"
[dependencies.raw-window-handle]
version = "0.6"
features = ["std"]
[dependencies.thiserror]
version = "2.0"
[dependencies.tracing]
version = "0.1"
optional = true
[dev-dependencies.base64]
version = "0.22"
[dev-dependencies.dom_query]
version = "0.27.0"
default-features = false
[dev-dependencies.getrandom]
version = "0.3"
[dev-dependencies.glow]
version = "0.16.0"
[dev-dependencies.http-range]
version = "0.1"
[dev-dependencies.libloading]
version = "0.8.9"
[dev-dependencies.percent-encoding]
version = "2.3"
[dev-dependencies.pollster]
version = "0.4.0"
[dev-dependencies.sha2]
version = "0.10"
[dev-dependencies.tao]
version = "0.35"
[dev-dependencies.wgpu]
version = "23"
[dev-dependencies.winit]
version = "0.30"
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.dirs]
version = "6"
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.gdkx11]
version = "0.18"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.gtk]
version = "0.18"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.javascriptcore-rs]
version = "=1.1.2"
features = ["v2_28"]
optional = true
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.percent-encoding]
version = "2.3"
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.soup3]
version = "0.5"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.webkit2gtk]
version = "=2.0.2"
features = ["v2_38"]
optional = true
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.webkit2gtk-sys]
version = "=2.0.2"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.x11-dl]
version = "2.21"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dev-dependencies.x11-dl]
version = "2.21"
[target.'cfg(target_os = "android")'.dependencies.base64]
version = "0.22"
[target.'cfg(target_os = "android")'.dependencies.crossbeam-channel]
version = "0.5"
[target.'cfg(target_os = "android")'.dependencies.dom_query]
version = "0.27.0"
default-features = false
[target.'cfg(target_os = "android")'.dependencies.jni]
version = "0.21"
[target.'cfg(target_os = "android")'.dependencies.libc]
version = "0.2"
[target.'cfg(target_os = "android")'.dependencies.ndk]
version = "0.9"
[target.'cfg(target_os = "android")'.dependencies.sha2]
version = "0.10"
[target.'cfg(target_os = "android")'.dependencies.tao-macros]
version = "0.1"
[target.'cfg(target_os = "ios")'.dependencies.objc2-ui-kit]
version = "0.3.0"
features = [
"std",
"objc2-core-foundation",
"UIResponder",
"UIScrollView",
"UIView",
"UIWindow",
"UIApplication",
"UIEvent",
"UIColor",
]
default-features = false
[target.'cfg(target_os = "macos")'.dependencies.objc2-app-kit]
version = "0.3.0"
features = [
"std",
"objc2-core-foundation",
"NSApplication",
"NSButton",
"NSControl",
"NSEvent",
"NSWindow",
"NSView",
"NSPasteboard",
"NSPanel",
"NSResponder",
"NSOpenPanel",
"NSSavePanel",
"NSMenu",
"NSGraphics",
"NSScreen",
]
default-features = false
[target.'cfg(target_os = "windows")'.dependencies.dunce]
version = "1"
[target.'cfg(target_os = "windows")'.dependencies.webview2-com]
version = "0.38"
[target.'cfg(target_os = "windows")'.dependencies.windows]
version = "0.61"
features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
"Win32_System_LibraryLoader",
"Win32_System_Ole",
"Win32_System_SystemInformation",
"Win32_System_SystemServices",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
"Win32_Globalization",
"Win32_UI_HiDpi",
"Win32_UI_Input",
"Win32_UI_Input_KeyboardAndMouse",
]
[target.'cfg(target_os = "windows")'.dependencies.windows-core]
version = "0.61"
[target.'cfg(target_os = "windows")'.dependencies.windows-version]
version = "0.1"
[target.'cfg(target_vendor = "apple")'.dependencies.block2]
version = "0.6"
[target.'cfg(target_vendor = "apple")'.dependencies.dirs]
version = "6"
[target.'cfg(target_vendor = "apple")'.dependencies.objc2]
version = "0.6.4"
features = [
"exception",
"disable-encoding-assertions",
]
[target.'cfg(target_vendor = "apple")'.dependencies.objc2-core-foundation]
version = "0.3.0"
features = [
"std",
"CFCGTypes",
]
default-features = false
[target.'cfg(target_vendor = "apple")'.dependencies.objc2-foundation]
version = "0.3.0"
features = [
"std",
"objc2-core-foundation",
"NSURLRequest",
"NSURL",
"NSString",
"NSKeyValueCoding",
"NSStream",
"NSDictionary",
"NSObject",
"NSData",
"NSEnumerator",
"NSKeyValueObserving",
"NSThread",
"NSJSONSerialization",
"NSDate",
"NSBundle",
"NSProcessInfo",
"NSValue",
"NSRange",
"NSRunLoop",
]
default-features = false
[target.'cfg(target_vendor = "apple")'.dependencies.objc2-web-kit]
version = "0.3.0"
features = [
"std",
"objc2-core-foundation",
"objc2-app-kit",
"block2",
"WKWebView",
"WKWebViewConfiguration",
"WKWebsiteDataStore",
"WKDownload",
"WKDownloadDelegate",
"WKNavigation",
"WKNavigationDelegate",
"WKUserContentController",
"WKURLSchemeHandler",
"WKPreferences",
"WKURLSchemeTask",
"WKScriptMessageHandler",
"WKUIDelegate",
"WKOpenPanelParameters",
"WKFrameInfo",
"WKSecurityOrigin",
"WKScriptMessage",
"WKNavigationAction",
"WKWebpagePreferences",
"WKNavigationResponse",
"WKUserScript",
"WKHTTPCookieStore",
"WKWindowFeatures",
]
default-features = false
[target.'cfg(target_vendor = "apple")'.dependencies.url]
version = "2.5"
[lints.rust.unexpected_cfgs]
level = "warn"
priority = 0
check-cfg = [
"cfg(linux)",
"cfg(gtk)",
]
+222
View File
@@ -0,0 +1,222 @@
workspace = {}
[package]
name = "wry"
version = "0.55.1"
authors = ["Tauri Programme within The Commons Conservancy"]
edition = "2021"
license = "Apache-2.0 OR MIT"
description = "Cross-platform WebView rendering library"
readme = "README.md"
repository = "https://github.com/tauri-apps/wry"
documentation = "https://docs.rs/wry"
categories = ["gui"]
rust-version = "1.77"
exclude = ["/.changes", "/.github", "/audits", "/wry-logo.svg"]
[package.metadata.docs.rs]
no-default-features = true
features = ["protocol", "os-webview"]
targets = [
"x86_64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
"x86_64-apple-darwin",
]
[features]
default = ["protocol", "os-webview", "x11"]
serde = ["dpi/serde"]
protocol = []
devtools = []
transparent = []
fullscreen = []
linux-body = ["webkit2gtk/v2_40", "os-webview"]
mac-proxy = []
os-webview = [
"javascriptcore-rs",
"webkit2gtk",
"webkit2gtk-sys",
"dep:gtk",
"soup3",
]
x11 = ["x11-dl", "gdkx11", "tao/x11"]
tracing = ["dep:tracing"]
[dependencies]
tracing = { version = "0.1", optional = true }
once_cell = "1"
thiserror = "2.0"
http = "1.1"
raw-window-handle = { version = "0.6", features = ["std"] }
dpi = "0.1"
cookie = "0.18"
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
javascriptcore-rs = { version = "=1.1.2", features = [
"v2_28",
], optional = true }
webkit2gtk = { version = "=2.0.2", features = ["v2_38"], optional = true }
webkit2gtk-sys = { version = "=2.0.2", optional = true }
gtk = { version = "0.18", optional = true }
soup3 = { version = "0.5", optional = true }
x11-dl = { version = "2.21", optional = true }
gdkx11 = { version = "0.18", optional = true }
percent-encoding = "2.3"
dirs = "6"
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dev-dependencies]
x11-dl = { version = "2.21" }
[target."cfg(target_os = \"windows\")".dependencies]
webview2-com = "0.38"
windows-version = "0.1"
windows-core = "0.61"
dunce = "1"
[target."cfg(target_os = \"windows\")".dependencies.windows]
version = "0.61"
features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
"Win32_System_LibraryLoader",
"Win32_System_Ole",
"Win32_System_SystemInformation",
"Win32_System_SystemServices",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
"Win32_Globalization",
"Win32_UI_HiDpi",
"Win32_UI_Input",
"Win32_UI_Input_KeyboardAndMouse",
]
[target.'cfg(target_vendor = "apple")'.dependencies]
url = "2.5"
dirs = "6"
block2 = "0.6"
objc2 = { version = "0.6.4", features = [
"exception",
# because `NSUUID::from_bytes` needs it,
# and `WebViewBuilderExtDarwin.with_data_store_identifier` crashes otherwise,
# see https://github.com/tauri-apps/tauri/issues/12843
"disable-encoding-assertions",
] }
objc2-web-kit = { version = "0.3.0", default-features = false, features = [
"std",
"objc2-core-foundation",
"objc2-app-kit",
"block2",
"WKWebView",
"WKWebViewConfiguration",
"WKWebsiteDataStore",
"WKDownload",
"WKDownloadDelegate",
"WKNavigation",
"WKNavigationDelegate",
"WKUserContentController",
"WKURLSchemeHandler",
"WKPreferences",
"WKURLSchemeTask",
"WKScriptMessageHandler",
"WKUIDelegate",
"WKOpenPanelParameters",
"WKFrameInfo",
"WKSecurityOrigin",
"WKScriptMessage",
"WKNavigationAction",
"WKWebpagePreferences",
"WKNavigationResponse",
"WKUserScript",
"WKHTTPCookieStore",
"WKWindowFeatures",
] }
objc2-core-foundation = { version = "0.3.0", default-features = false, features = [
"std",
"CFCGTypes",
] }
objc2-foundation = { version = "0.3.0", default-features = false, features = [
"std",
"objc2-core-foundation",
"NSURLRequest",
"NSURL",
"NSString",
"NSKeyValueCoding",
"NSStream",
"NSDictionary",
"NSObject",
"NSData",
"NSEnumerator",
"NSKeyValueObserving",
"NSThread",
"NSJSONSerialization",
"NSDate",
"NSBundle",
"NSProcessInfo",
"NSValue",
"NSRange",
"NSRunLoop",
] }
[target.'cfg(target_os = "ios")'.dependencies]
objc2-ui-kit = { version = "0.3.0", default-features = false, features = [
"std",
"objc2-core-foundation",
"UIResponder",
"UIScrollView",
"UIView",
"UIWindow",
"UIApplication",
"UIEvent",
"UIColor",
] }
[target.'cfg(target_os = "macos")'.dependencies]
objc2-app-kit = { version = "0.3.0", default-features = false, features = [
"std",
"objc2-core-foundation",
"NSApplication",
"NSButton",
"NSControl",
"NSEvent",
"NSWindow",
"NSView",
"NSPasteboard",
"NSPanel",
"NSResponder",
"NSOpenPanel",
"NSSavePanel",
"NSMenu",
"NSGraphics",
"NSScreen",
] }
[target."cfg(target_os = \"android\")".dependencies]
crossbeam-channel = "0.5"
sha2 = "0.10"
base64 = "0.22"
jni = "0.21"
ndk = "0.9"
tao-macros = "0.1"
libc = "0.2"
dom_query = { version = "0.27.0", default-features = false }
[dev-dependencies]
pollster = "0.4.0"
tao = "0.35"
wgpu = "23"
winit = "0.30"
getrandom = "0.3"
http-range = "0.1"
percent-encoding = "2.3"
glow = "0.16.0"
libloading = "0.8.9"
# For unit testing `inject_initialization_scripts` since we can't do it easily on Android
sha2 = "0.10"
base64 = "0.22"
dom_query = { version = "0.27.0", default-features = false }
[lints.rust.unexpected_cfgs]
level = "warn"
check-cfg = ["cfg(linux)", "cfg(gtk)"]
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2023 Ngo Iok Ui & Tauri Programme within The Commons Conservancy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+20
View File
@@ -0,0 +1,20 @@
SPDXVersion: SPDX-2.1
DataLicense: CC0-1.0
PackageName: wry
DataFormat: SPDXRef-1
PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy
PackageHomePage: https://tauri.app
PackageLicenseDeclared: Apache-2.0
PackageLicenseDeclared: MIT
PackageCopyrightText: 2020-2023, The Tauri Programme in the Commons Conservancy
PackageSummary: <text>Wry is the official, rust-based webview
windowing service for Tauri.
</text>
PackageComment: <text>The package includes the following libraries; see
Relationship information.
</text>
Created: 2020-05-20T09:00:00Z
PackageDownloadLocation: git://github.com/tauri-apps/wry
PackageDownloadLocation: git+https://github.com/tauri-apps/wry.git
PackageDownloadLocation: git+ssh://github.com/tauri-apps/wry.git
Creator: Person: Daniel Thompson-Yvetot
+332
View File
@@ -0,0 +1,332 @@
# Mobile Setup for Wry
We use [cargo-mobile2](https://github.com/tauri-apps/cargo-mobile2) to create a mobile project for both Xcode and Android studio.
## Prerequisite
- Works on **Linux**, **Windows**, **macOS**, and **WSL**(Windows subsystem for Linux).
- **Xcode** and [**Android Studio**](https://developer.android.com/studio) installed properly. This is **the most difficult** part IMHO. This means all toolchains and SDK are all installed. Please report an issue with **comprehensive** steps if you encounter any problem.
## Setting up Android Environment
### 1. Installing JDK
#### Using Android Studio:
If you have Android Studio installed, it ships with a version of JDK so you don't have to install it manually. It is usually at `<path of android studio installation>/jre`. It will be used for `JAVA_HOME` env var.
> On macOS, it can be found at `/Applications/Android\ Studio.app/Contents/jbr/Contents/Home`
> On Windows, it can be found at `C:\Program Files\Android\Android Studio\jre`
#### Using the terminal:
##### Linux/WSL
- Install it by running the following command based on your distro to install JDK:
- debian-based
```
sudo apt install default-jdk
```
- arch-based
```
sudo pacman -S jdk-openjdk
```
- Set the `JAVA_HOME` env variable for this current shell (we will make it permanent later on)
```bash
export JAVA_HOME="/usr/lib/jvm/java-11-openjdk-amd64"
```
#### macOS
- Install openjdk from Homebrew:
```
brew install openjdk
```
- Link to system Java wrapper and set the `JAVA_HOME` env:
```
sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk
export JAVA_HOME="/Library/Java/JavaVirtualMachines/openjdk.jdk/Contents/Home"
```
##### Windows
- Download openjdk-11
```powershell
cd $HOME\downloads
Invoke-WebRequest https://download.java.net/java/GA/jdk11/9/GPL/openjdk-11.0.2_windows-x64_bin.zip -o openjdk-11.zip
Expand-Archive openjdk-11.zip -d .
mkdir $env:LocalAppData\Java
mv jdk-11.0.2 $env:LocalAppData\Java
```
- Set the `JAVA_HOME` env variable for this current shell (we will make it permanent later on)
```powershell
$env:JAVA_HOME="$env:LocalAppData\Java\jdk-11.0.2"
```
### 2. Installing Android SDK and NDK
There are two ways to install the sdk and ndk.
#### Using Android Studio:
You can use the SDK Manager in Android Studio to install:
1. Android Sdk Platform 33
2. Android SDK Platform-Tools
3. NDK (Side by side) 25.0.8775105
4. Android SDK Build-Tools 33.0.
5. Android SDK Command-line Tools
> Note: you may need to tick `Show Package Details` in the right bottom corner to be able to see some of these components
#### Using the terminal:
If you don't want or can't use Android Studio you can still get the SDK Manager cli quite easily and use it to install other components.
> Note: The SDK Manager is part of the "Command line tools only" that can be downloaded from [here](https://developer.android.com/studio#command-tools)
##### Linux/WSL/macOS
Download the `cmdline-tools`
```bash
cd ~/Downloads
# if you are on Linux/WSL:
wget https://dl.google.com/android/repository/commandlinetools-linux-8512546_latest.zip -O
# if you are on macos:
wget https://dl.google.com/android/repository/commandlinetools-mac-8512546_latest.zip -O
unzip cmdline-tools.zip
cd cmdline-tools
mkdir latest
mv bin latest/
mv lib latest/
mv NOTICE.txt latest/
mv source.properties latest/
cd ..
mkdir ~/.android # You can use another location for your SDK but I prefer using ~/.android
mv cmdline-tools ~/.android
```
Install required SDK and NDK components
```bash
export ANDROID_HOME="$HOME/.android"
~/.android/cmdline-tools/latest/bin/sdkmanager "platforms;android-33" "platform-tools" "ndk;25.0.8775105" "build-tools;33.0.0"
# Install the emulator if you plan on using a virtual device later
~/.android/cmdline-tools/latest/bin/sdkmanager "emulator"
```
##### Windows
Download the `cmdline-tools`
```powershell
cd $HOME\downloads
Invoke-WebRequest https://dl.google.com/android/repository/commandlinetools-win-8512546_latest.zip -o cmdline-tools.zip
Expand-Archive cmdline-tools.zip -d .
cd cmdline-tools
mkdir latest
mv bin latest/
mv lib latest/
mv NOTICE.txt latest/
mv source.properties latest/
cd ..
mkdir $HOME\.android # You can use another location for your SDK but I prefer using $HOME\.android
mv cmdline-tools $HOME\.android
```
Install required SDK and NDK components
```powershell
$env:ANDROID_HOME="$HOME\.android"
&"$env:ANDROID_HOME\cmdline-tools\latest\bin\sdkmanager.exe" "platforms;android-33" "platform-tools" "ndk;25.0.8775105" "build-tools;33.0.0"
# Install the emulator if you plan on using a virtual device later
&"$env:ANDROID_HOME\cmdline-tools\latest\bin\sdkmanager.exe" "emulator"
```
> Note: the location you moved the `cmdline-tools` directory into will be the location of your android SDK.
### 3. Setting up Environment Variables
You'll need to set up some environment variables to get everything to work properly. The environment variables below should be all the ones your need to be able to use [cargo-mobile2](https://github.com/tauri-apps/cargo-mobile2) to build/run your android app.
##### Linux/WSL/macOS
- Setting `JAVA_HOME`:
```bash
# In .bashrc or .zshrc:
export JAVA_HOME="/usr/lib/jvm/java-11-openjdk-amd64"
# If you are using Android studio, on Linux, it is:
export JAVA_HOME=/opt/android-studio/jre
# And on macOS, it is:
export JAVA_HOME=/Applications/Android\ Studio.app/Contents/jbr/Contents/Home
```
- Setting `ANDROID_HOME`:
```bash
export ANDROID_HOME="$HOME/.android"
# If you are using Android studio, on Linux, it is:
export ANDROID_HOME="$HOME/Android/Sdk"
# And on macOS, it is:
export ANDROID_HOME="$HOME/Library/Android/sdk"
```
- Setting `PATH`:
```bash
export NDK_HOME="$ANDROID_HOME/ndk/25.0.8775105" # The patch version might be different
export PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin"
export PATH="$PATH:$ANDROID_HOME/platform-tools"
```
> For WSL:
> you also need to get ADB to connect to your emulator that is running on Windows
>
> ```bash
> export WSL_HOST="192.168.1.2" # Run `ipconfig` in windows to get your computer IP
> export ADB_SERVER_SOCKET=tcp:$WSL_HOST:5037
> ```
After updating `.bashrc` either run `source ~/.bashrc` or reopen your terminal to apply the changes.
##### Windows
Open a powershell instance and run the following commands in order
```powershell
Function Add-EnvVar($name, $value) { [System.Environment]::SetEnvironmentVariable("$name", "$value", "User") }
Function Add-PATHEntry($path) { $newPath = [System.Environment]::GetEnvironmentVariable("Path", "User") + ";" + $path; [System.Environment]::SetEnvironmentVariable("Path", "$newPath", "User") }
Add-EnvVar JAVA_HOME "$env:LocalAppData\Java\jdk-11.0.2" # if you are using Android studio, the location is different, see the section above about JDK
$env:SDK_ROOT="$HOME\.android"# if you are using Android studio, the sdk location will be at `$env:LocalAppData\Android\Sdk`
Add-EnvVar ANDROID_HOME "$env:SDK_ROOT"
Add-EnvVar NDK_HOME "$env:SDK_ROOT\ndk\25.0.8775105"
Add-PATHEntry "$env:SDK_ROOT\cmdline-tools\latest\bin"
Add-PATHEntry "$env:SDK_ROOT\platform-tools"
```
> IMPORTANT: you need to reboot your Windows machine in order for the environement variables to be loaded correctly.
You should now have all the environment variables required and the cmdline-tools available in your PATH. You can verify this by running `sdkmanager` which should now be showing its help info.
### 4. Install Rust android targets:
```shell
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
```
## Getting Started
Now lets bootstrap a project to develop a tauri or wry project for mobile.
- Install [cargo-mobile2](https://github.com/tauri-apps/cargo-mobile2) CLI by running:
```bash
cargo install --git https://github.com/tauri-apps/cargo-mobile2
```
- Create a directory and init the project.
```bash
mkdir hello
cd hello
cargo mobile init
# Project name (hello):
# Stylized name (Hello):
# Domain (example.com): tauri.app
# Detected template packs:
# [0] bevy
# [1] bevy-demo
# [2] wgpu
# [3] winit
# [4] wry
# Enter an index for a template pack above.
# Template pack (0): 4
```
## Build and Run on Device
### Android
> Make sure you're device is connected to adb
> you can check by running `cargo android list` or `adb devices`
- `cargo android run`
### iOS
- `cargo build --target aarch64-apple-ios`
- `cargo apple run`
First time running the app will be blocked. Go to your phone's `Settings > Privacy & Security > Developer Mode` to enable developer mode. And then go to `Settings -> General -> VPN and device management -> From "Developer App"` section to press "Apple Development: APPLE_ID" -> Trust.
## Build and Run on Emulator
### Android
##### Using Android Studio
- Open the project in Android Studio `cargo android open`
- Click `Trust Project`, `Use Embedded JDK`
- Choose an emulator. I usually choose Pixel 4 API 32
- (optional) if you face this error `Device supports x86, but APK only supports armeabi-v7a` then check this [Stack Overflow answer](https://stackoverflow.com/questions/41775988/what-is-the-reason-for-the-error-device-supports-x86-but-apk-only-supports-arm/43742161#43742161) to fix it.
- Press run button.
##### Without Android Studio
If you don't have access to Android Studio or don't want or when running in WSL, you can build and run the generated project directly from the terminal
1. List available emulators
- Linux/WSL/macOS:
```bash
$ANDROID_HOME/emulator/emulator -list-avds
```
- Windows:
```powershell
&"$env:ANDROID_HOME\emulator\emulator" -list-avds
```
you should now see a list of available emulators like the following, you'll need one of them for the next step:
```
Resizable_API_33
Pixel_5_API_33
```
2. Start the emulator with the name of the desired emulator:
- Linux/WSL/macOS:
```bash
$ANDROID_HOME/emulator/emulator -avd Resizable_API_33
```
- Windows:
```powershell
&"$env:ANDROID_HOME\emulator\emulator" -avd Resizable_API_33
```
3. In a new terminal window, run:
```bash
cargo android run
```
### iOS
- If you are on x86_64: `cargo build --target x86_64-apple-ios`
- If you are on M1: `cargo build --target aarch64-apple-ios-sim`
- `cargo apple open`
- Choose a simulator.
- Press run button.
## Devtools
Set `devtools` attribute to true when building webview.
### Android
Open `chrome://inspect/#devices` in Chrome to get the devtools window.
### iOS
Open Safari > Develop > [Your Device Name] > [Your WebView].
+317
View File
@@ -0,0 +1,317 @@
<p align="center"><img height="100" src="https://raw.githubusercontent.com/tauri-apps/wry/refs/heads/dev/.github/splash.png" alt="WRY Webview Rendering library" /></p>
[![](https://img.shields.io/crates/v/wry?style=flat-square)](https://crates.io/crates/wry) [![](https://img.shields.io/docsrs/wry?style=flat-square)](https://docs.rs/wry/)
[![License](https://img.shields.io/badge/License-MIT%20or%20Apache%202-green.svg)](https://opencollective.com/tauri)
[![Chat Server](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/SpmNs4S)
[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app)
[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation)
[![support](https://img.shields.io/badge/sponsor-Open%20Collective-blue.svg)](https://opencollective.com/tauri)
Wry is a cross-platform WebView rendering library.
The webview requires a running event loop and a window type that implements [`HasWindowHandle`],
or a gtk container widget if you need to support X11 and Wayland.
You can use a windowing library like [`tao`] or [`winit`].
### Examples
This example leverages the [`HasWindowHandle`] and supports Windows, macOS, iOS, Android and Linux (X11 Only).
See the following example using [`winit`]:
```rust
#[derive(Default)]
struct App {
window: Option<Window>,
webview: Option<wry::WebView>,
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let window = event_loop.create_window(Window::default_attributes()).unwrap();
let webview = WebViewBuilder::new()
.with_url("https://tauri.app")
.build(&window)
.unwrap();
self.window = Some(window);
self.webview = Some(webview);
}
fn window_event(&mut self, _event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {}
}
let event_loop = EventLoop::new().unwrap();
let mut app = App::default();
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.
See the following example using [`tao`]:
```rust
let event_loop = EventLoop::new();
let window = WindowBuilder::new().build(&event_loop).unwrap();
let builder = WebViewBuilder::new().with_url("https://tauri.app");
#[cfg(not(target_os = "linux"))]
let webview = builder.build(&window).unwrap();
#[cfg(target_os = "linux")]
let webview = builder.build_gtk(window.gtk_window()).unwrap();
```
### Child webviews
You can use [`WebViewBuilder::build_as_child`] to create the webview as a child inside another window. This is supported on
macOS, Windows and Linux (X11 Only).
```rust
#[derive(Default)]
struct App {
window: Option<Window>,
webview: Option<wry::WebView>,
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let window = event_loop.create_window(Window::default_attributes()).unwrap();
let webview = WebViewBuilder::new()
.with_url("https://tauri.app")
.with_bounds(Rect {
position: LogicalPosition::new(100, 100).into(),
size: LogicalSize::new(200, 200).into(),
})
.build_as_child(&window)
.unwrap();
self.window = Some(window);
self.webview = Some(webview);
}
fn window_event(&mut self, _event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {}
}
let event_loop = EventLoop::new().unwrap();
let mut app = App::default();
event_loop.run_app(&mut app).unwrap();
```
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`].
```rust
let event_loop = EventLoop::new();
let window = WindowBuilder::new().build(&event_loop).unwrap();
let builder = WebViewBuilder::new()
.with_url("https://tauri.app")
.with_bounds(Rect {
position: LogicalPosition::new(100, 100).into(),
size: LogicalSize::new(200, 200).into(),
});
#[cfg(not(target_os = "linux"))]
let webview = builder.build_as_child(&window).unwrap();
#[cfg(target_os = "linux")]
let webview = {
# use gtk::prelude::*;
let vbox = window.default_vbox().unwrap(); // tao adds a gtk::Box by default
let fixed = gtk::Fixed::new();
fixed.show_all();
vbox.pack_start(&fixed, true, true, 0);
builder.build_gtk(&fixed).unwrap()
};
```
### Platform Considerations
Here is the underlying web engine each platform uses, and some dependencies you might need to install.
#### Linux
[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
your windowing library event loop.
```rust
#[derive(Default)]
struct App {
webview_window: Option<(Window, WebView)>,
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let window = event_loop.create_window(Window::default_attributes()).unwrap();
let webview = WebViewBuilder::new()
.with_url("https://tauri.app")
.build(&window)
.unwrap();
self.webview_window = Some((window, webview));
}
fn window_event(&mut self, _event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {}
// Advance GTK event loop <!----- IMPORTANT
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
#[cfg(target_os = "linux")]
while gtk::events_pending() {
gtk::main_iteration_do(false);
}
}
}
let event_loop = EventLoop::new().unwrap();
let mut app = App::default();
event_loop.run_app(&mut app).unwrap();
```
##### Linux Dependencies
###### Arch Linux / Manjaro:
```bash
sudo pacman -S webkit2gtk-4.1
```
###### Debian / Ubuntu:
```bash
sudo apt install libwebkit2gtk-4.1-dev
```
###### Fedora
```bash
sudo dnf install gtk3-devel webkit2gtk4.1-devel
```
###### Nix & NixOS
```nix
# shell.nix
let
# Unstable Channel | Rolling Release
pkgs = import (fetchTarball("channel:nixpkgs-unstable")) { };
packages = with pkgs; [
pkg-config
webkitgtk_4_1
];
in
pkgs.mkShell {
buildInputs = packages;
}
```
```sh
nix-shell shell.nix
```
###### GUIX
```scheme
;; manifest.scm
(specifications->manifest
'("pkg-config" ; Helper tool used when compiling
"webkitgtk" ; Web content engine fot GTK+
))
```
```bash
guix shell -m manifest.scm
```
#### macOS
WebKit is native on macOS so everything should be fine.
If you are cross-compiling for macOS using [osxcross](https://github.com/tpoechtrager/osxcross) and encounter a runtime panic like `Class with name WKWebViewConfiguration could not be found` it's possible that `WebKit.framework` has not been linked correctly, to fix this set the `RUSTFLAGS` environment variable:
```bash
RUSTFLAGS="-l framework=WebKit" cargo build --target=x86_64-apple-darwin --release
```
#### Windows
WebView2 provided by Microsoft Edge Chromium is used. So wry supports Windows 7, 8, 10 and 11.
#### Android
In order for `wry` to be able to create webviews on Android, there are a few requirements that your application needs to uphold:
1. You need to set a few environment variables that will be used to generate the necessary kotlin
files that you need to include in your Android application for wry to function properly.
- `WRY_ANDROID_PACKAGE`: which is the reversed domain name of your android project and the app name in snake_case, for example, `com.wry.example.wry_app`
- `WRY_ANDROID_LIBRARY`: for example, if your cargo project has a lib name `wry_app`, it will generate `libwry_app.so` so you set this env var to `wry_app`
- `WRY_ANDROID_KOTLIN_FILES_OUT_DIR`: for example, `path/to/app/src/main/kotlin/com/wry/example`
2. Your main Android Activity needs to inherit `AppCompatActivity`, preferably it should use the generated `WryActivity` or inherit it.
3. Your Rust app needs to call `wry::android_setup` function to setup the necessary logic to be able to create webviews later on.
4. Your Rust app needs to call `wry::android_binding!` macro to setup the JNI functions that will be called by `WryActivity` and various other places.
It is recommended to use the [`tao`](https://docs.rs/tao/latest/tao/) crate as it provides maximum compatibility with `wry`.
```rust
#[cfg(target_os = "android")]
{
tao::android_binding!(
com_example,
wry_app,
WryActivity,
wry::android_setup, // pass the wry::android_setup function to tao which will be invoked when the event loop is created
_start_app
);
wry::android_binding!(com_example, ttt);
}
```
If this feels overwhelming, you can just use the preconfigured template from [`cargo-mobile2`](https://github.com/tauri-apps/cargo-mobile2).
For more information, check out [MOBILE.md](https://github.com/tauri-apps/wry/blob/dev/MOBILE.md).
### Feature flags
Wry uses a set of feature flags to toggle several advanced features.
- `os-webview` (default): Enables the default WebView framework on the platform. This must be enabled
for the crate to work. This feature was added in preparation of other ports like cef and servo.
- `protocol` (default): Enables [`WebViewBuilder::with_custom_protocol`] to define custom URL scheme for handling tasks like
loading assets.
- `drag-drop` (default): Enables [`WebViewBuilder::with_drag_drop_handler`] to control the behavior when there are files
interacting with the window.
- `devtools`: Enables devtools on release builds. Devtools are always enabled in debug builds.
On **macOS**, enabling devtools, requires calling private APIs so you should not enable this flag in release
build if your app needs to publish to App Store.
- `transparent`: Transparent background on **macOS** requires calling private functions.
Avoid this in release build if your app needs to publish to App Store.
- `fullscreen`: Fullscreen video and other media on **macOS** requires calling private functions.
Avoid this in release build if your app needs to publish to App Store.
- `linux-body`: Enables body support of custom protocol request on Linux. Requires
WebKit2GTK v2.40 or above.
- `tracing`: enables [`tracing`] for `evaluate_script`, `ipc_handler`, and `custom_protocols`.
### Partners
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://crabnebula.dev" target="_blank">
<img src=".github/sponsors/crabnebula.svg" alt="CrabNebula" width="283">
</a>
</td>
</tr>
</tbody>
</table>
For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri).
### License
Apache-2.0/MIT
[`tao`]: https://docs.rs/tao
[`winit`]: https://docs.rs/winit
[`tracing`]: https://docs.rs/tracing
+19
View File
@@ -0,0 +1,19 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| > 1.0 | :white_check_mark: |
| < 1.0 | :x: |
## Reporting a Vulnerability
If you have found a potential security threat, vulnerability or exploit in Tauri
or one of its upstream dependencies, please DON’T create a pull-request, DON’T
file an issue on GitHub, DON’T mention it on Discord and DON’T create a forum thread.
We will be adding contact information to this page very soon.
At the current time we do not have the financial ability to reward bounties,
but in extreme cases will at our discretion consider a reward.
+118
View File
@@ -0,0 +1,118 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
fn main() {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os == "macos" || target_os == "ios" {
println!("cargo:rustc-link-lib=framework=WebKit");
}
if target_os == "android" {
use std::{fs, path::PathBuf};
fn env_var(var: &str) -> String {
std::env::var(var).unwrap_or_else(|_| {
panic!("`{var}` is not set, which is needed to generate the kotlin files for android.")
})
}
println!("cargo:rerun-if-env-changed=WRY_ANDROID_PACKAGE");
println!("cargo:rerun-if-env-changed=WRY_ANDROID_LIBRARY");
println!("cargo:rerun-if-env-changed=WRY_ANDROID_KOTLIN_FILES_OUT_DIR");
if let Ok(kotlin_out_dir) = std::env::var("WRY_ANDROID_KOTLIN_FILES_OUT_DIR") {
let package = env_var("WRY_ANDROID_PACKAGE");
let library = env_var("WRY_ANDROID_LIBRARY");
let kotlin_out_dir = PathBuf::from(&kotlin_out_dir)
.canonicalize()
.unwrap_or_else(move |_| {
panic!("Failed to canonicalize `WRY_ANDROID_KOTLIN_FILES_OUT_DIR` path {kotlin_out_dir}")
});
let kotlin_files_path =
PathBuf::from(env_var("CARGO_MANIFEST_DIR")).join("src/android/kotlin");
println!("cargo:rerun-if-changed={}", kotlin_files_path.display());
let kotlin_files = fs::read_dir(kotlin_files_path).expect("failed to read kotlin directory");
for file in kotlin_files {
let file = file.unwrap();
let class_extension_env = format!(
"WRY_{}_CLASS_EXTENSION",
file
.path()
.file_stem()
.unwrap()
.to_string_lossy()
.to_uppercase()
);
let class_init_env = format!(
"WRY_{}_CLASS_INIT",
file
.path()
.file_stem()
.unwrap()
.to_string_lossy()
.to_uppercase()
);
println!("cargo:rerun-if-env-changed={class_extension_env}");
println!("cargo:rerun-if-env-changed={class_init_env}");
let content = fs::read_to_string(file.path())
.expect("failed to read kotlin file as string")
.replace("{{package}}", &package)
.replace("{{package-unescaped}}", &package.replace('`', ""))
.replace("{{library}}", &library)
.replace(
"{{class-extension}}",
&std::env::var(&class_extension_env).unwrap_or_default(),
)
.replace(
"{{class-init}}",
&std::env::var(&class_init_env).unwrap_or_default(),
);
let auto_generated_comment = match file
.path()
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
{
"pro" => "# THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!!\n\n",
"kt" => "/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */\n\n",
_ => "String::new()",
};
let mut out = String::from(auto_generated_comment);
out.push_str(&content);
let out_path = kotlin_out_dir.join(file.file_name());
// Overwrite only if changed to not trigger rebuilds
if fs::read_to_string(&out_path).map_or(true, |o| o != out) {
fs::write(&out_path, out).expect("Failed to write kotlin file");
}
println!("cargo:rerun-if-changed={}", out_path.display());
}
}
}
let target = std::env::var("TARGET").unwrap_or_default();
let android = target.contains("android");
let linux = !android
&& (target.contains("linux")
|| target.contains("freebsd")
|| target.contains("dragonfly")
|| target.contains("netbsd")
|| target.contains("openbsd"));
alias("linux", linux);
alias("gtk", cfg!(feature = "os-webview") && linux);
}
fn alias(alias: &str, condition: bool) {
if condition {
println!("cargo:rustc-cfg={alias}");
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": ["config:recommended"],
"rangeStrategy": "replace",
"packageRules": [
{
"semanticCommitType": "chore",
"matchPackageNames": ["*"]
}
]
}
+15
View File
@@ -0,0 +1,15 @@
max_width = 100
hard_tabs = false
tab_spaces = 2
newline_style = "Unix"
use_small_heuristics = "Default"
reorder_imports = true
reorder_modules = true
remove_nested_parens = true
edition = "2018"
merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false
force_explicit_abi = true
imports_granularity = "Crate"
#license_template_path = ".license_template"
+483
View File
@@ -0,0 +1,483 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use http::{
header::{HeaderName, HeaderValue, CONTENT_LENGTH, CONTENT_TYPE},
Request,
};
use jni::errors::Result as JniResult;
pub use jni::{
self,
objects::{GlobalRef, JClass, JMap, JObject, JString},
sys::{jboolean, jint, jobject, jstring},
JNIEnv,
};
pub use ndk;
use ndk::looper::{FdEvent, ThreadLooper};
use std::os::fd::{AsFd, AsRawFd};
use super::{
main_pipe::{MainPipe, MAIN_PIPE},
ASSET_LOADER_DOMAIN, EVAL_CALLBACKS, IPC, ON_LOAD_HANDLER, REQUEST_HANDLER, TITLE_CHANGE_HANDLER,
URL_LOADING_OVERRIDE, WITH_ASSET_LOADER,
};
use crate::PageLoadEvent;
#[macro_export]
macro_rules! android_binding {
($domain:ident, $package:ident) => {
::wry::android_binding!($domain, $package, ::wry)
};
// use imported `android_setup` just to force the import path to use `wry::{}`
// as the macro breaks without braces
($domain:ident, $package:ident, $wry:path) => {{
use $wry::{android_setup as _, prelude::*};
android_fn!($domain, $package, Rust, wryCreate, []);
android_fn!(
$domain,
$package,
Rust,
onWebviewDestroy,
[JObject, JString]
);
android_fn!(
$domain,
$package,
Rust,
handleRequest,
[JString, JObject, jboolean],
jobject
);
android_fn!(
$domain,
$package,
Rust,
withAssetLoader,
[JString],
jboolean
);
android_fn!(
$domain,
$package,
Rust,
assetLoaderDomain,
[JString],
jstring
);
android_fn!(
$domain,
$package,
Rust,
shouldOverride,
[JString, JString],
jboolean
);
android_fn!($domain, $package, Rust, onEval, [JString, jint, JString]);
android_fn!($domain, $package, Rust, onPageLoading, [JString, JString]);
android_fn!($domain, $package, Rust, onPageLoaded, [JString, JString]);
android_fn!($domain, $package, Rust, ipc, [JString, JString, JString]);
android_fn!(
$domain,
$package,
Rust,
handleReceivedTitle,
[JString, JString],
);
}};
}
fn handle_request(
env: &mut JNIEnv,
webview_id: JString,
request: JObject,
is_document_start_script_enabled: jboolean,
) -> JniResult<jobject> {
let webview_id = env.get_string(&webview_id)?;
let webview_id = webview_id.to_str().ok().unwrap_or_default();
if let Some(handler) = REQUEST_HANDLER.lock().unwrap().get(webview_id) {
#[cfg(feature = "tracing")]
let span =
tracing::info_span!(parent: None, "wry::custom_protocol::handle", uri = tracing::field::Empty).entered();
let mut request_builder = Request::builder();
let uri = env
.call_method(&request, "getUrl", "()Landroid/net/Uri;", &[])?
.l()?;
let url: JString = env
.call_method(&uri, "toString", "()Ljava/lang/String;", &[])?
.l()?
.into();
let url = env.get_string(&url)?.to_string_lossy().to_string();
#[cfg(feature = "tracing")]
span.record("uri", &url);
request_builder = request_builder.uri(&url);
let method = env
.call_method(&request, "getMethod", "()Ljava/lang/String;", &[])?
.l()
.map(JString::from)?;
request_builder = request_builder.method(
env
.get_string(&method)?
.to_string_lossy()
.to_string()
.as_str(),
);
let request_headers = env
.call_method(request, "getRequestHeaders", "()Ljava/util/Map;", &[])?
.l()?;
let request_headers = JMap::from_env(env, &request_headers)?;
let mut iter = request_headers.iter(env)?;
while let Some((header, value)) = iter.next(env)? {
let header = JString::from(header);
let value = JString::from(value);
let header = env.get_string(&header)?;
let value = env.get_string(&value)?;
if let (Ok(header), Ok(value)) = (
HeaderName::from_bytes(header.to_bytes()),
HeaderValue::from_bytes(value.to_bytes()),
) {
request_builder = request_builder.header(header, value);
}
}
let final_request = match request_builder.body(Vec::new()) {
Ok(req) => req,
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to build response: {_e}");
return Ok(*JObject::null());
}
};
let response = {
#[cfg(feature = "tracing")]
let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered();
(handler.handler)(
webview_id,
final_request,
is_document_start_script_enabled != 0,
)
};
if let Some(response) = response {
let status = response.status();
let status_code = status.as_u16() as i32;
let status_err = if status_code < 100 {
Some("Status code can't be less than 100")
} else if status_code > 599 {
Some("statusCode can't be greater than 599.")
} else if status_code > 299 && status_code < 400 {
Some("statusCode can't be in the [300, 399] range.")
} else {
None
};
if let Some(_err) = status_err {
#[cfg(feature = "tracing")]
tracing::warn!("{_err}");
return Ok(*JObject::null());
}
let reason_phrase = status.canonical_reason().unwrap_or("OK");
let (mime_type, encoding) = if let Some(content_type) = response.headers().get(CONTENT_TYPE) {
let content_type = content_type.to_str().unwrap().trim();
let mut s = content_type.split(';');
let mime_type = s.next().unwrap().trim();
let mut encoding = None;
for token in s {
let token = token.trim();
if token.starts_with("charset=") {
encoding.replace(token.split('=').nth(1).unwrap());
break;
}
}
(
env.new_string(mime_type)?,
if let Some(encoding) = encoding {
env.new_string(encoding)?
} else {
JString::default()
},
)
} else {
(JString::default(), JString::default())
};
let headers = response.headers();
let obj = env.new_object("java/util/HashMap", "()V", &[])?;
let response_headers = {
let headers_map = JMap::from_env(env, &obj)?;
for (name, value) in headers.iter() {
// WebResourceResponse will automatically generate Content-Type and
// Content-Length headers so we should skip them to avoid duplication.
if name == CONTENT_TYPE || name == CONTENT_LENGTH {
continue;
}
let key = env.new_string(name)?;
let value = env.new_string(value.to_str().unwrap_or_default())?;
headers_map.put(env, &key, &value)?;
}
headers_map
};
let bytes = response.body();
let byte_array_input_stream = env.find_class("java/io/ByteArrayInputStream")?;
let byte_array = env.byte_array_from_slice(bytes)?;
let stream = env.new_object(byte_array_input_stream, "([B)V", &[(&byte_array).into()])?;
let reason_phrase = env.new_string(reason_phrase)?;
let web_resource_response_class = env.find_class("android/webkit/WebResourceResponse")?;
let web_resource_response = env.new_object(
web_resource_response_class,
"(Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/util/Map;Ljava/io/InputStream;)V",
&[(&mime_type).into(), (&encoding).into(), status_code.into(), (&reason_phrase).into(), (&response_headers).into(), (&stream).into()],
)?;
return Ok(*web_resource_response);
}
}
Ok(*JObject::null())
}
#[allow(non_snake_case)]
pub unsafe fn wryCreate(env: JNIEnv, _: JClass) {
let mut main_pipe = MainPipe { env };
let looper = ThreadLooper::for_thread().unwrap();
looper
.add_fd_with_callback(MAIN_PIPE[0].as_fd(), FdEvent::INPUT, move |fd, _event| {
let size = std::mem::size_of::<bool>();
let mut wake = false;
if libc::read(fd.as_raw_fd(), &mut wake as *mut _ as *mut _, size) == size as libc::ssize_t {
// unregister itself on errors
main_pipe.recv().is_ok()
} else {
// unregister itself
false
}
})
.unwrap();
}
#[allow(non_snake_case)]
pub unsafe fn onWebviewDestroy(mut env: JNIEnv, _: JClass, activity: JObject, webview_id: JString) {
let activity_id = env
.call_method(&activity, "getId", "()I", &[])
.unwrap()
.i()
.unwrap();
let webview_id = env
.get_string(&webview_id)
.unwrap()
.to_string_lossy()
.to_string();
let is_changing_configurations = env
.call_method(&activity, "isChangingConfigurations", "()Z", &[])
.unwrap()
.z()
.unwrap();
super::MainPipe::send(
activity_id,
super::WebViewMessage::OnDestroy {
activity_id,
webview_id,
is_changing_configurations,
},
);
}
#[allow(non_snake_case)]
pub unsafe fn handleRequest(
mut env: JNIEnv,
_: JClass,
webview_id: JString,
request: JObject,
is_document_start_script_enabled: jboolean,
) -> jobject {
match handle_request(
&mut env,
webview_id,
request,
is_document_start_script_enabled,
) {
Ok(response) => response,
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to handle request: {_e}");
JObject::null().as_raw()
}
}
}
#[allow(non_snake_case)]
pub unsafe fn shouldOverride(
mut env: JNIEnv,
_: JClass,
webview_id: JString,
url: JString,
) -> jboolean {
match env.get_string(&url) {
Ok(url) => {
let url = url.to_string_lossy().to_string();
let Ok(webview_id) = env.get_string(&webview_id) else {
return false.into();
};
let webview_id = webview_id.to_str().ok().unwrap_or_default();
URL_LOADING_OVERRIDE
.lock()
.unwrap()
.get(webview_id)
// We negate the result of the function because the logic for the android
// client is different from how the navigation_handler is defined.
//
// https://developer.android.com/reference/android/webkit/WebViewClient#shouldOverrideUrlLoading(android.webkit.WebView,%20android.webkit.WebResourceRequest)
.map(|f| !(f.handler)(url))
.unwrap_or_default()
}
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}");
false
}
}
.into()
}
#[allow(non_snake_case)]
pub unsafe fn onEval(mut env: JNIEnv, _: JClass, _webview_id: JString, id: jint, result: JString) {
match env.get_string(&result) {
Ok(result) => {
if let Some(cb) = EVAL_CALLBACKS
.get_or_init(Default::default)
.lock()
.unwrap()
.get(&id)
{
cb(result.into());
}
}
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}");
}
}
}
pub unsafe fn ipc(mut env: JNIEnv, _: JClass, webview_id: JString, url: JString, body: JString) {
match (
env.get_string(&url),
env.get_string(&body),
env.get_string(&webview_id),
) {
(Ok(url), Ok(body), Ok(webview_id)) => {
#[cfg(feature = "tracing")]
let _span = tracing::info_span!(parent: None, "wry::ipc::handle").entered();
let url = url.to_string_lossy().to_string();
let body = body.to_string_lossy().to_string();
let webview_id = webview_id.to_string_lossy().to_string();
if let Some(ipc) = IPC.lock().unwrap().get(&webview_id) {
(ipc.handler)(Request::builder().uri(url).body(body).unwrap())
}
}
(Err(_e), _, _) | (_, Err(_e), _) | (_, _, Err(_e)) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}")
}
}
}
#[allow(non_snake_case)]
pub unsafe fn handleReceivedTitle(mut env: JNIEnv, _: JClass, webview_id: JString, title: JString) {
match (env.get_string(&title), env.get_string(&webview_id)) {
(Ok(title), Ok(webview_id)) => {
let title = title.to_string_lossy().to_string();
let webview_id = webview_id.to_string_lossy().to_string();
if let Some(title_handler) = TITLE_CHANGE_HANDLER.lock().unwrap().get(&webview_id) {
(title_handler.handler)(title)
}
}
(Err(_e), _) | (_, Err(_e)) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}")
}
}
}
#[allow(non_snake_case)]
pub unsafe fn withAssetLoader(mut env: JNIEnv, _: JClass, webview_id: JString) -> jboolean {
let Ok(webview_id) = env.get_string(&webview_id) else {
return false.into();
};
let webview_id = webview_id.to_str().ok().unwrap_or_default();
(*WITH_ASSET_LOADER
.lock()
.unwrap()
.get(webview_id)
.unwrap_or(&false))
.into()
}
#[allow(non_snake_case)]
pub unsafe fn assetLoaderDomain(mut env: JNIEnv, _: JClass, webview_id: JString) -> jstring {
let Ok(webview_id) = env.get_string(&webview_id) else {
return env.new_string("wry.assets").unwrap().as_raw();
};
let webview_id = webview_id.to_str().ok().unwrap_or_default();
if let Some(domain) = ASSET_LOADER_DOMAIN.lock().unwrap().get(webview_id) {
env.new_string(domain).unwrap().as_raw()
} else {
env.new_string("wry.assets").unwrap().as_raw()
}
}
#[allow(non_snake_case)]
pub unsafe fn onPageLoading(mut env: JNIEnv, _: JClass, webview_id: JString, url: JString) {
match (env.get_string(&url), env.get_string(&webview_id)) {
(Ok(url), Ok(webview_id)) => {
let url = url.to_string_lossy().to_string();
let webview_id = webview_id.to_string_lossy().to_string();
if let Some(on_load) = ON_LOAD_HANDLER.lock().unwrap().get(&webview_id) {
(on_load.handler)(PageLoadEvent::Started, url)
}
}
(Err(_e), _) | (_, Err(_e)) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}")
}
}
}
#[allow(non_snake_case)]
pub unsafe fn onPageLoaded(mut env: JNIEnv, _: JClass, webview_id: JString, url: JString) {
match (env.get_string(&url), env.get_string(&webview_id)) {
(Ok(url), Ok(webview_id)) => {
let url = url.to_string_lossy().to_string();
let webview_id = webview_id.to_string_lossy().to_string();
if let Some(on_load) = ON_LOAD_HANDLER.lock().unwrap().get(&webview_id) {
(on_load.handler)(PageLoadEvent::Finished, url)
}
}
(Err(_e), _) | (_, Err(_e)) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}")
}
}
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
@file:Suppress("unused")
package {{package}}
import android.webkit.*
class Ipc(val webView: RustWebView, val webViewClient: RustWebViewClient) {
@JavascriptInterface
fun postMessage(message: String?) {
message?.let {m ->
// we're not using WebView::getUrl() here because it needs to be executed on the main thread
// and it would slow down the Ipc
// so instead we track the current URL on the webview client
Rust.ipc(webView.id, webViewClient.currentUrl, m)
}
}
{{class-extension}}
}
+87
View File
@@ -0,0 +1,87 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package {{package}}
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/Logger.java
import android.text.TextUtils
import android.util.Log
class Logger {
companion object {
private const val LOG_TAG_CORE = "Tauri"
fun tags(vararg subtags: String): String {
return if (subtags.isNotEmpty()) {
LOG_TAG_CORE + "/" + TextUtils.join("/", subtags)
} else LOG_TAG_CORE
}
fun verbose(message: String) {
verbose(LOG_TAG_CORE, message)
}
private fun verbose(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.v(tag, message)
}
fun debug(message: String) {
debug(LOG_TAG_CORE, message)
}
fun debug(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.d(tag, message)
}
fun info(message: String) {
info(LOG_TAG_CORE, message)
}
fun info(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.i(tag, message)
}
fun warn(message: String) {
warn(LOG_TAG_CORE, message)
}
fun warn(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.w(tag, message)
}
fun error(message: String) {
error(LOG_TAG_CORE, message, null)
}
fun error(message: String, e: Throwable?) {
error(LOG_TAG_CORE, message, e)
}
fun error(tag: String, message: String, e: Throwable?) {
if (!shouldLog()) {
return
}
Log.e(tag, message, e)
}
private fun shouldLog(): Boolean {
return BuildConfig.DEBUG
}
}
}
@@ -0,0 +1,115 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package {{package}}
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/PermissionHelper.java
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.ActivityCompat
import java.util.ArrayList
object PermissionHelper {
/**
* Checks if a list of given permissions are all granted by the user
*
* @param permissions Permissions to check.
* @return True if all permissions are granted, false if at least one is not.
*/
fun hasPermissions(context: Context?, permissions: Array<String>): Boolean {
for (perm in permissions) {
if (ActivityCompat.checkSelfPermission(
context!!,
perm
) != PackageManager.PERMISSION_GRANTED
) {
return false
}
}
return true
}
/**
* Check whether the given permission has been defined in the AndroidManifest.xml
*
* @param permission A permission to check.
* @return True if the permission has been defined in the Manifest, false if not.
*/
fun hasDefinedPermission(context: Context, permission: String): Boolean {
var hasPermission = false
val requestedPermissions = getManifestPermissions(context)
if (!requestedPermissions.isNullOrEmpty()) {
val requestedPermissionsList = listOf(*requestedPermissions)
val requestedPermissionsArrayList = ArrayList(requestedPermissionsList)
if (requestedPermissionsArrayList.contains(permission)) {
hasPermission = true
}
}
return hasPermission
}
/**
* Check whether all of the given permissions have been defined in the AndroidManifest.xml
* @param context the app context
* @param permissions a list of permissions
* @return true only if all permissions are defined in the AndroidManifest.xml
*/
fun hasDefinedPermissions(context: Context, permissions: Array<String>): Boolean {
for (permission in permissions) {
if (!hasDefinedPermission(context, permission)) {
return false
}
}
return true
}
/**
* Get the permissions defined in AndroidManifest.xml
*
* @return The permissions defined in AndroidManifest.xml
*/
private fun getManifestPermissions(context: Context): Array<String>? {
var requestedPermissions: Array<String>? = null
try {
val pm = context.packageManager
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
} else {
@Suppress("DEPRECATION")
pm.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)
}
if (packageInfo != null) {
requestedPermissions = packageInfo.requestedPermissions
}
} catch (_: Exception) {
}
return requestedPermissions
}
/**
* Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml
*
* @param neededPermissions The permissions needed.
* @return The permissions not present in AndroidManifest.xml
*/
fun getUndefinedPermissions(context: Context, neededPermissions: Array<String?>): Array<String?> {
val undefinedPermissions = ArrayList<String?>()
val requestedPermissions = getManifestPermissions(context)
if (!requestedPermissions.isNullOrEmpty()) {
val requestedPermissionsList = listOf(*requestedPermissions)
val requestedPermissionsArrayList = ArrayList(requestedPermissionsList)
for (permission in neededPermissions) {
if (!requestedPermissionsArrayList.contains(permission)) {
undefinedPermissions.add(permission)
}
}
var undefinedPermissionArray = arrayOfNulls<String>(undefinedPermissions.size)
undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray)
return undefinedPermissionArray
}
return neededPermissions
}
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
@file:Suppress("unused")
package {{package}}
import android.content.Intent
import android.webkit.WebView
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
object Rust {
init {
System.loadLibrary("{{library}}")
}
@JvmStatic external fun onActivityCreate(activity: WryActivity)
@JvmStatic external fun onActivityDestroy(activity: WryActivity)
@JvmStatic external fun onActivitySaveInstanceState()
@JvmStatic external fun onActivityLowMemory()
@JvmStatic external fun onWindowFocusChanged(activity: WryActivity, focus: Boolean)
@JvmStatic external fun onNewIntent(intent: Intent)
@JvmStatic external fun create()
@JvmStatic external fun start()
@JvmStatic external fun resume()
@JvmStatic external fun pause()
@JvmStatic external fun stop()
@JvmStatic external fun wryCreate()
@JvmStatic external fun onWebviewDestroy(activity: WryActivity, webviewId: String)
@JvmStatic external fun ipc(webviewId: String, url: String, message: String)
@JvmStatic external fun assetLoaderDomain(webviewId: String): String
@JvmStatic external fun withAssetLoader(webviewId: String): Boolean
@JvmStatic external fun handleRequest(webviewId: String, request: WebResourceRequest, isDocumentStartScriptEnabled: Boolean): WebResourceResponse?
@JvmStatic external fun shouldOverride(webviewId: String, url: String): Boolean
@JvmStatic external fun onPageLoading(webviewId: String, url: String)
@JvmStatic external fun onPageLoaded(webviewId: String, url: String)
@JvmStatic external fun onEval(webviewId: String, id: Int, result: String)
@JvmStatic external fun handleReceivedTitle(webviewId: String, title: String)
}
@@ -0,0 +1,491 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
@file:Suppress("ObsoleteSdkInt", "RedundantOverride", "QueryPermissionsNeeded", "SimpleDateFormat")
package {{package}}
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java
import android.Manifest
import android.app.Activity
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
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<Array<String>>
private var activityLauncher: ActivityResultLauncher<Intent>
private var permissionListener: PermissionListener? = null
private var activityListener: ActivityResultListener? = null
init {
activity = appActivity
val permissionCallback =
ActivityResultCallback { isGranted: Map<String, Boolean> ->
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)
}
}
}
/**
* Render web content in `view`.
*
* Both this method and [.onHideCustomView] are required for
* rendering web content in full screen.
*
* @see [](https://developer.android.com/reference/android/webkit/WebChromeClient.onShowCustomView
) */
override fun onShowCustomView(view: View, callback: CustomViewCallback) {
callback.onCustomViewHidden()
super.onShowCustomView(view, callback)
}
/**
* Render web content in the original Web View again.
*
* Do not remove this method--@see #onShowCustomView(View, CustomViewCallback).
*/
override fun onHideCustomView() {
super.onHideCustomView()
}
override fun onPermissionRequest(request: PermissionRequest) {
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")) {
permissionList.add(Manifest.permission.CAMERA)
}
if (listOf(*request.resources).contains("android.webkit.resource.AUDIO_CAPTURE")) {
permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS)
permissionList.add(Manifest.permission.RECORD_AUDIO)
}
if (permissionList.isNotEmpty() && isRequestPermissionRequired) {
val permissions = permissionList.toTypedArray()
permissionListener = object : PermissionListener {
override fun onPermissionSelect(isGranted: Boolean?) {
if (isGranted == true) {
request.grant(request.resources)
} else {
request.deny()
}
}
}
permissionLauncher.launch(permissions)
} else {
request.grant(request.resources)
}
}
/**
* Show the browser alert modal
* @param view
* @param url
* @param message
* @param result
* @return
*/
override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean {
if (activity.isFinishing) {
return true
}
val builder = AlertDialog.Builder(view.context)
builder
.setMessage(message)
.setPositiveButton(
"OK"
) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
result.confirm()
}
.setOnCancelListener { dialog: DialogInterface ->
dialog.dismiss()
result.cancel()
}
val dialog = builder.create()
dialog.show()
return true
}
/**
* Show the browser confirm modal
* @param view
* @param url
* @param message
* @param result
* @return
*/
override fun onJsConfirm(view: WebView, url: String, message: String, result: JsResult): Boolean {
if (activity.isFinishing) {
return true
}
val builder = AlertDialog.Builder(view.context)
builder
.setMessage(message)
.setPositiveButton(
"OK"
) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
result.confirm()
}
.setNegativeButton(
"Cancel"
) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
result.cancel()
}
.setOnCancelListener { dialog: DialogInterface ->
dialog.dismiss()
result.cancel()
}
val dialog = builder.create()
dialog.show()
return true
}
/**
* Show the browser prompt modal
* @param view
* @param url
* @param message
* @param defaultValue
* @param result
* @return
*/
override fun onJsPrompt(
view: WebView,
url: String,
message: String,
defaultValue: String,
result: JsPromptResult
): Boolean {
if (activity.isFinishing) {
return true
}
val builder = AlertDialog.Builder(view.context)
val input = EditText(view.context)
builder
.setMessage(message)
.setView(input)
.setPositiveButton(
"OK"
) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
val inputText1 = input.text.toString().trim { it <= ' ' }
result.confirm(inputText1)
}
.setNegativeButton(
"Cancel"
) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
result.cancel()
}
.setOnCancelListener { dialog: DialogInterface ->
dialog.dismiss()
result.cancel()
}
val dialog = builder.create()
dialog.show()
return true
}
/**
* Handle the browser geolocation permission prompt
* @param origin
* @param callback
*/
override fun onGeolocationPermissionsShowPrompt(
origin: String,
callback: GeolocationPermissions.Callback
) {
super.onGeolocationPermissionsShowPrompt(origin, callback)
Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: $origin")
val geoPermissions =
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
if (!PermissionHelper.hasPermissions(activity, geoPermissions)) {
permissionListener = object : PermissionListener {
override fun onPermissionSelect(isGranted: Boolean?) {
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 {
callback.invoke(origin, false, false)
}
}
}
}
permissionLauncher.launch(geoPermissions)
} else {
// permission is already granted
callback.invoke(origin, true, false)
Logger.debug("onGeolocationPermissionsShowPrompt: has required permission")
}
}
override fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri?>?>,
fileChooserParams: FileChooserParams
): Boolean {
val acceptTypes = listOf(*fileChooserParams.acceptTypes)
val captureEnabled = fileChooserParams.isCaptureEnabled
val capturePhoto = captureEnabled && acceptTypes.contains("image/*")
val captureVideo = captureEnabled && acceptTypes.contains("video/*")
if (capturePhoto || captureVideo) {
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)
permissionLauncher.launch(camPermission)
}
} else {
showFilePicker(filePathCallback, fileChooserParams)
}
return true
}
private val isMediaCaptureSupported: Boolean
get() {
val permissions = arrayOf(Manifest.permission.CAMERA)
return PermissionHelper.hasPermissions(activity, permissions) ||
!PermissionHelper.hasDefinedPermission(activity, Manifest.permission.CAMERA)
}
private fun showMediaCaptureOrFilePicker(
filePathCallback: ValueCallback<Array<Uri?>?>,
fileChooserParams: FileChooserParams,
isVideo: Boolean
) {
val isVideoCaptureSupported = true
val shown = if (isVideo && isVideoCaptureSupported) {
showVideoCapturePicker(filePathCallback)
} else {
showImageCapturePicker(filePathCallback)
}
if (!shown) {
Logger.warn(
Logger.tags("FileChooser"),
"Media capture intent could not be launched. Falling back to default file picker."
)
showFilePicker(filePathCallback, fileChooserParams)
}
}
private fun showImageCapturePicker(filePathCallback: ValueCallback<Array<Uri?>?>): Boolean {
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
if (takePictureIntent.resolveActivity(activity.packageManager) == null) {
return false
}
val imageFileUri: Uri = try {
createImageFileUri()
} catch (ex: Exception) {
Logger.error("Unable to create temporary media capture file: " + ex.message)
return false
}
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri)
activityListener = object : ActivityResultListener {
override fun onActivityResult(result: ActivityResult?) {
var res: Array<Uri?>? = null
if (result?.resultCode == Activity.RESULT_OK) {
res = arrayOf(imageFileUri)
}
filePathCallback.onReceiveValue(res)
}
}
activityLauncher.launch(takePictureIntent)
return true
}
private fun showVideoCapturePicker(filePathCallback: ValueCallback<Array<Uri?>?>): Boolean {
val takeVideoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
if (takeVideoIntent.resolveActivity(activity.packageManager) == null) {
return false
}
activityListener = object : ActivityResultListener {
override fun onActivityResult(result: ActivityResult?) {
var res: Array<Uri?>? = null
if (result?.resultCode == Activity.RESULT_OK) {
res = arrayOf(result.data!!.data)
}
filePathCallback.onReceiveValue(res)
}
}
activityLauncher.launch(takeVideoIntent)
return true
}
private fun showFilePicker(
filePathCallback: ValueCallback<Array<Uri?>?>,
fileChooserParams: FileChooserParams
) {
val intent = fileChooserParams.createIntent()
if (fileChooserParams.mode == FileChooserParams.MODE_OPEN_MULTIPLE) {
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}
if (fileChooserParams.acceptTypes.size > 1 || intent.type!!.startsWith(".")) {
val validTypes = getValidTypes(fileChooserParams.acceptTypes)
intent.putExtra(Intent.EXTRA_MIME_TYPES, validTypes)
if (intent.type!!.startsWith(".")) {
intent.type = validTypes[0]
}
}
try {
activityListener = object : ActivityResultListener {
override fun onActivityResult(result: ActivityResult?) {
val res: Array<Uri?>?
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
)
}
filePathCallback.onReceiveValue(res)
}
}
activityLauncher.launch(intent)
} catch (e: ActivityNotFoundException) {
filePathCallback.onReceiveValue(null)
}
}
private fun getValidTypes(currentTypes: Array<String>): Array<String> {
val validTypes: MutableList<String> = ArrayList()
val mtm = MimeTypeMap.getSingleton()
for (mime in currentTypes) {
if (mime.startsWith(".")) {
val extension = mime.substring(1)
val extensionMime = mtm.getMimeTypeFromExtension(extension)
if (extensionMime != null && !validTypes.contains(extensionMime)) {
validTypes.add(extensionMime)
}
} else if (!validTypes.contains(mime)) {
validTypes.add(mime)
}
}
val validObj: Array<Any> = validTypes.toTypedArray()
return Arrays.copyOf(
validObj, validObj.size,
Array<String>::class.java
)
}
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
val tag: String = Logger.tags("Console")
if (consoleMessage.message() != null && isValidMsg(consoleMessage.message())) {
val msg = String.format(
"File: %s - Line %d - Msg: %s",
consoleMessage.sourceId(),
consoleMessage.lineNumber(),
consoleMessage.message()
)
val level = consoleMessage.messageLevel().name
if ("ERROR".equals(level, ignoreCase = true)) {
Logger.error(tag, msg, null)
} else if ("WARNING".equals(level, ignoreCase = true)) {
Logger.warn(tag, msg)
} else if ("TIP".equals(level, ignoreCase = true)) {
Logger.debug(tag, msg)
} else {
Logger.info(tag, msg)
}
}
return true
}
private fun isValidMsg(msg: String): Boolean {
return !(msg.contains("%cresult %c") ||
msg.contains("%cnative %c") ||
msg.equals("[object Object]", ignoreCase = true) ||
msg.equals("console.groupEnd", ignoreCase = true))
}
@Throws(IOException::class)
private fun createImageFileUri(): Uri {
val photoFile = createImageFile(activity)
return FileProvider.getUriForFile(
activity,
activity.packageName.toString() + ".fileprovider",
photoFile
)
}
@Throws(IOException::class)
private fun createImageFile(activity: Activity): File {
// Create an image file name
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
val imageFileName = "JPEG_" + timeStamp + "_"
val storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile(imageFileName, ".jpg", storageDir)
}
override fun onReceivedTitle(
view: WebView,
title: String
) {
Rust.handleReceivedTitle((view as RustWebView).id, title)
}
}
+96
View File
@@ -0,0 +1,96 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
@file:Suppress("unused", "SetJavaScriptEnabled")
package {{package}}
import android.annotation.SuppressLint
import android.webkit.*
import android.content.Context
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
import kotlin.collections.Map
@SuppressLint("RestrictedApi")
class RustWebView(context: Context, val initScripts: Array<String>, val id: String): WebView(context) {
val isDocumentStartScriptEnabled: Boolean
init {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.setGeolocationEnabled(true)
settings.databaseEnabled = true
settings.mediaPlaybackRequiresUserGesture = false
settings.javaScriptCanOpenWindowsAutomatically = true
if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
isDocumentStartScriptEnabled = true
for (script in initScripts) {
WebViewCompat.addDocumentStartJavaScript(this, script, setOf("*"));
}
} else {
isDocumentStartScriptEnabled = false
}
{{class-init}}
}
fun loadUrlMainThread(url: String) {
post {
loadUrl(url)
}
}
fun loadUrlMainThread(url: String, additionalHttpHeaders: Map<String, String>) {
post {
loadUrl(url, additionalHttpHeaders)
}
}
override fun loadUrl(url: String) {
if (!Rust.shouldOverride(id, url)) {
super.loadUrl(url);
}
}
override fun loadUrl(url: String, additionalHttpHeaders: Map<String, String>) {
if (!Rust.shouldOverride(id, url)) {
super.loadUrl(url, additionalHttpHeaders);
}
}
fun loadHTMLMainThread(html: String) {
post {
super.loadData(html, "text/html", null)
}
}
fun evalScript(id: Int, script: String) {
post {
super.evaluateJavascript(script) { result ->
Rust.onEval(this.id, id, result)
}
}
}
fun clearAllBrowsingData() {
try {
super.getContext().deleteDatabase("webviewCache.db")
super.getContext().deleteDatabase("webview.db")
super.clearCache(true)
super.clearHistory()
super.clearFormData()
} catch (ex: Exception) {
Logger.error("Unable to create temporary media capture file: " + ex.message)
}
}
fun getCookies(url: String): String {
val cookieManager = CookieManager.getInstance()
return cookieManager.getCookie(url)
}
{{class-extension}}
}
@@ -0,0 +1,98 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package {{package}}
import android.net.Uri
import android.webkit.*
import android.content.Context
import android.graphics.Bitmap
import android.os.Handler
import android.os.Looper
import androidx.webkit.WebViewAssetLoader
class RustWebViewClient(webView: RustWebView, context: Context): WebViewClient() {
private val interceptedState = mutableMapOf<String, Boolean>()
var currentUrl: String = "about:blank"
private var lastInterceptedUrl: Uri? = null
private var pendingUrlRedirect: String? = null
private val assetLoader = WebViewAssetLoader.Builder()
.setDomain(Rust.assetLoaderDomain(webView.id))
.addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(context))
.build()
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
pendingUrlRedirect?.let {
Handler(Looper.getMainLooper()).post {
view.loadUrl(it)
}
pendingUrlRedirect = null
return null
}
lastInterceptedUrl = request.url
return if (Rust.withAssetLoader((view as RustWebView).id)) {
assetLoader.shouldInterceptRequest(request.url)
} else {
val response = Rust.handleRequest(view.id, request, view.isDocumentStartScriptEnabled)
if (response != null) {
if (response.responseHeaders != null) {
response.responseHeaders["Cache-Control"] = "no-store"
} else {
response.responseHeaders = mapOf("Cache-Control" to "no-store")
}
}
interceptedState[request.url.toString()] = response != null
return response
}
}
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
return Rust.shouldOverride((view as RustWebView).id, request.url.toString())
}
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
currentUrl = url
if (interceptedState[url] == false) {
val webView = view as RustWebView
for (script in webView.initScripts) {
view.evaluateJavascript(script, null)
}
}
return Rust.onPageLoading((view as RustWebView).id, url)
}
override fun onPageFinished(view: WebView, url: String) {
Rust.onPageLoaded((view as RustWebView).id, url)
}
override fun onReceivedError(
view: WebView,
request: WebResourceRequest,
error: WebResourceError
) {
// we get a net::ERR_CONNECTION_REFUSED when an external URL redirects to a custom protocol
// e.g. oauth flow, because shouldInterceptRequest is not called on redirects
// so we must force retry here with loadUrl() to get a chance of the custom protocol to kick in
if (error.errorCode == ERROR_CONNECT && request.isForMainFrame && request.url != lastInterceptedUrl) {
// prevent the default error page from showing
view.stopLoading()
// without this initial loadUrl the app is stuck
view.loadUrl(request.url.toString())
// ensure the URL is actually loaded - for some reason there's a race condition and we need to call loadUrl() again later
pendingUrlRedirect = request.url.toString()
} else {
super.onReceivedError(view, request, error)
}
}
{{class-extension}}
}
+173
View File
@@ -0,0 +1,173 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package {{package}}
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.webkit.WebView
import android.view.KeyEvent
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
private val ACTIVITY_ID_KEY = "__wryActivityId"
object WryLifecycleObserver : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
Rust.create()
Rust.wryCreate()
}
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
Rust.start()
}
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
Rust.resume()
}
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
Rust.pause()
}
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
Rust.stop()
}
}
abstract class WryActivity : AppCompatActivity() {
private lateinit var mWebView: RustWebView
var id: Int = 0
open val handleBackNavigation: Boolean = true
open fun onWebViewCreate(webView: WebView) { }
fun setWebView(webView: RustWebView) {
mWebView = webView
if (handleBackNavigation) {
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (this@WryActivity::mWebView.isInitialized) {
if (this@WryActivity.mWebView.canGoBack()) {
this@WryActivity.mWebView.goBack()
} else {
this.isEnabled = false
this@WryActivity.onBackPressed()
this.isEnabled = true
}
}
}
}
onBackPressedDispatcher.addCallback(this, callback)
}
onWebViewCreate(webView)
}
val version: String
@SuppressLint("WebViewApiAvailability", "ObsoleteSdkInt")
get() {
// Check getCurrentWebViewPackage() directly if above Android 8
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return WebView.getCurrentWebViewPackage()?.versionName ?: ""
}
// Otherwise manually check WebView versions
var webViewPackage = "com.google.android.webview"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
webViewPackage = "com.android.chrome"
}
try {
@Suppress("DEPRECATION")
val info = packageManager.getPackageInfo(webViewPackage, 0)
return info.versionName.toString()
} catch (ex: Exception) {
Logger.warn("Unable to get package info for '$webViewPackage'$ex")
}
try {
@Suppress("DEPRECATION")
val info = packageManager.getPackageInfo("com.android.webview", 0)
return info.versionName.toString()
} catch (ex: Exception) {
Logger.warn("Unable to get package info for 'com.android.webview'$ex")
}
// Could not detect any webview, return empty string
return ""
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
id = savedInstanceState?.getInt(ACTIVITY_ID_KEY) ?: intent.extras?.getInt(ACTIVITY_ID_KEY) ?: hashCode()
ProcessLifecycleOwner.get().lifecycle.addObserver(WryLifecycleObserver)
Rust.onActivityCreate(this)
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
Rust.onWindowFocusChanged(this, hasFocus)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(ACTIVITY_ID_KEY, id)
Rust.onActivitySaveInstanceState()
}
override fun onPause() {
super.onPause()
if (::mWebView.isInitialized) {
mWebView.onPause()
}
}
override fun onResume() {
super.onResume()
if (::mWebView.isInitialized) {
mWebView.onResume()
}
}
override fun onDestroy() {
super.onDestroy()
Rust.onActivityDestroy(this)
Rust.onWebviewDestroy(this, if (::mWebView.isInitialized) { mWebView.id } else { "" })
}
override fun onLowMemory() {
super.onLowMemory()
Rust.onActivityLowMemory()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Rust.onNewIntent(intent)
}
fun getAppClass(name: String): Class<*> {
return Class.forName(name)
}
fun startActivity(cls: Class<*>): Int {
val intent = Intent(this, cls)
val id = kotlin.random.Random.nextInt()
intent.putExtra(ACTIVITY_ID_KEY, id)
startActivity(intent)
return id
}
{{class-extension}}
}
@@ -0,0 +1,35 @@
# Copyright 2020-2023 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
-keep class {{package-unescaped}}.* {
native <methods>;
}
-keep class {{package-unescaped}}.WryActivity {
public <init>(...);
void setWebView({{package-unescaped}}.RustWebView);
java.lang.Class getAppClass(...);
int getId();
java.lang.String getVersion();
int startActivity(...);
}
-keep class {{package-unescaped}}.Ipc {
public <init>(...);
@android.webkit.JavascriptInterface public <methods>;
}
-keep class {{package-unescaped}}.RustWebView {
public <init>(...);
void loadUrlMainThread(...);
void loadHTMLMainThread(...);
void evalScript(...);
}
-keep class {{package-unescaped}}.RustWebChromeClient,{{package-unescaped}}.RustWebViewClient {
public <init>(...);
}
+607
View File
@@ -0,0 +1,607 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{Error, InitializationScript, RGBA};
use crossbeam_channel::*;
use jni::{
errors::Result as JniResult,
objects::{GlobalRef, JMap, JObject, JString},
JNIEnv, JavaVM,
};
use once_cell::sync::Lazy;
use std::{
collections::BTreeMap,
ffi::c_void,
os::unix::prelude::*,
sync::{Arc, Mutex},
};
use super::{find_class, EvalCallback, WebviewId, EVAL_CALLBACKS, EVAL_ID_GENERATOR, PACKAGE};
pub type ActivityId = i32;
static CHANNEL: Lazy<(
Sender<(ActivityId, WebViewMessage)>,
Receiver<(ActivityId, WebViewMessage)>,
)> = Lazy::new(|| bounded(8));
pub static MAIN_PIPE: Lazy<[OwnedFd; 2]> = Lazy::new(|| {
let mut pipe: [RawFd; 2] = Default::default();
unsafe { libc::pipe(pipe.as_mut_ptr()) };
unsafe { pipe.map(|fd| OwnedFd::from_raw_fd(fd)) }
});
#[derive(Clone)]
pub struct ActivityProxy {
pub activity: GlobalRef,
pub window_manager: GlobalRef,
pub webview: Option<GlobalRef>,
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 {
Self {
activity,
window_manager,
webview: None,
webchrome_client,
java_vm: vm.get_java_vm_pointer() as *mut _,
}
}
}
static ACTIVITY_PROXY: once_cell::sync::Lazy<Mutex<BTreeMap<ActivityId, ActivityProxy>>> =
Lazy::new(|| Mutex::new(BTreeMap::new()));
pub fn activity_proxy(id: ActivityId) -> Option<ActivityProxy> {
ACTIVITY_PROXY.lock().unwrap().get(&id).cloned()
}
fn remove_activity_proxy(id: ActivityId) {
ACTIVITY_PROXY.lock().unwrap().remove(&id);
}
pub fn register_activity_proxy(
vm: JavaVM,
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);
activity_proxy.insert(id, proxy.clone());
}
}
pub fn activity_id_for_window_manager(window_manager: JObject) -> Option<ActivityId> {
for (activity_id, proxy) in ACTIVITY_PROXY.lock().unwrap().iter() {
let vm = unsafe { JavaVM::from_raw(proxy.java_vm.cast()) }.unwrap();
let mut env = vm.attach_current_thread_as_daemon().unwrap();
let equals = env
.call_method(
proxy.window_manager.as_obj(),
"equals",
"(Ljava/lang/Object;)Z",
&[(&window_manager).into()],
)
.and_then(|v| v.z())
.unwrap_or_default();
if equals {
return Some(*activity_id);
}
}
None
}
pub fn first_activity_id() -> Option<ActivityId> {
ACTIVITY_PROXY.lock().unwrap().keys().next().cloned()
}
pub fn get_webview(activity_id: ActivityId) -> Option<GlobalRef> {
ACTIVITY_PROXY
.lock()
.unwrap()
.get(&activity_id)
.unwrap()
.webview
.as_ref()
.cloned()
}
pub struct MainPipe<'a> {
pub env: JNIEnv<'a>,
}
impl<'a> MainPipe<'a> {
pub(crate) fn send(activity_id: ActivityId, message: WebViewMessage) {
let size = std::mem::size_of::<bool>();
if CHANNEL.0.send((activity_id, message)).is_ok() {
unsafe {
libc::write(
MAIN_PIPE[1].as_raw_fd(),
&true as *const _ as *const _,
size,
)
};
}
}
pub fn recv(&mut self) -> JniResult<()> {
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 {
#[cfg(debug_assertions)]
eprintln!("no activity found for activity id: {}", activity_id);
return Ok(());
};
let CreateWebViewAttributes {
url,
html,
#[cfg(any(debug_assertions, feature = "devtools"))]
devtools,
transparent,
background_color,
headers,
on_webview_created,
autoplay,
user_agent,
initialization_scripts,
id,
javascript_disabled,
..
} = attrs;
let string_class = self.env.find_class("java/lang/String")?;
let initialization_scripts_array = self.env.new_object_array(
initialization_scripts.len() as i32,
string_class,
self.env.new_string("")?,
)?;
for (i, init_script) in initialization_scripts.into_iter().enumerate() {
self.env.set_object_array_element(
&initialization_scripts_array,
i as i32,
self.env.new_string(init_script.script)?,
)?;
}
let id = self.env.new_string(id)?;
// Create webview
let rust_webview_class = find_class(
&mut self.env,
&activity,
format!("{}/RustWebView", PACKAGE.get().unwrap()),
)?;
let webview = self.env.new_object(
&rust_webview_class,
"(Landroid/content/Context;[Ljava/lang/String;Ljava/lang/String;)V",
&[
(&activity).into(),
(&initialization_scripts_array).into(),
(&id).into(),
],
)?;
// get settings
let web_settings = self
.env
.call_method(
&webview,
"getSettings",
"()Landroid/webkit/WebSettings;",
&[],
)?
.l()?;
// set media autoplay
self.env.call_method(
&web_settings,
"setMediaPlaybackRequiresUserGesture",
"(Z)V",
&[(!autoplay).into()],
)?;
// set user-agent
if let Some(user_agent) = user_agent {
let user_agent = self.env.new_string(user_agent)?;
self.env.call_method(
&web_settings,
"setUserAgentString",
"(Ljava/lang/String;)V",
&[(&user_agent).into()],
)?;
}
// disable javascript
if javascript_disabled {
self.env.call_method(
&web_settings,
"setJavaScriptEnabled",
"(Z)V",
&[false.into()],
)?;
}
let webview_class_name = format!("{}/RustWebView", PACKAGE.get().unwrap());
self.env.call_method(
&activity,
"setWebView",
format!("(L{webview_class_name};)V"),
&[(&webview).into()],
)?;
// Navigation
if let Some(u) = url {
if let Ok(url) = self.env.new_string(u) {
load_url(&mut self.env, &webview, &url, headers, true)?;
}
} else if let Some(h) = html {
if let Ok(html) = self.env.new_string(h) {
load_html(&mut self.env, &webview, &html)?;
}
}
// Enable devtools
#[cfg(any(debug_assertions, feature = "devtools"))]
self.env.call_static_method(
&rust_webview_class,
"setWebContentsDebuggingEnabled",
"(Z)V",
&[devtools.into()],
)?;
if transparent {
set_background_color(&mut self.env, &webview, (0, 0, 0, 0))?;
} else if let Some(color) = background_color {
set_background_color(&mut self.env, &webview, color)?;
}
// Create and set webview client
let client_class_name = format!("{}/RustWebViewClient", PACKAGE.get().unwrap());
let rust_webview_client_class =
find_class(&mut self.env, &activity, client_class_name.clone())?;
let webview_client = self.env.new_object(
&rust_webview_client_class,
format!("(L{webview_class_name};Landroid/content/Context;)V"),
&[(&webview).into(), (&activity).into()],
)?;
self.env.call_method(
&webview,
"setWebViewClient",
"(Landroid/webkit/WebViewClient;)V",
&[(&webview_client).into()],
)?;
// set webchrome client
self.env.call_method(
&webview,
"setWebChromeClient",
"(Landroid/webkit/WebChromeClient;)V",
&[web_chrome_client.as_obj().into()],
)?;
// Add javascript interface (IPC)
let ipc_class = find_class(
&mut self.env,
&activity,
format!("{}/Ipc", PACKAGE.get().unwrap()),
)?;
let ipc = self.env.new_object(
ipc_class,
format!("(L{webview_class_name};L{client_class_name};)V"),
&[(&webview).into(), (&webview_client).into()],
)?;
let ipc_str = self.env.new_string("ipc")?;
self.env.call_method(
&webview,
"addJavascriptInterface",
"(Ljava/lang/Object;Ljava/lang/String;)V",
&[(&ipc).into(), (&ipc_str).into()],
)?;
// Set content view
self.env.call_method(
&activity,
"setContentView",
"(Landroid/view/View;)V",
&[(&webview).into()],
)?;
if let Some(on_webview_created) = on_webview_created {
if let Err(_e) = on_webview_created(super::Context {
env: &mut self.env,
activity: &activity,
webview: &webview,
}) {
#[cfg(feature = "tracing")]
tracing::warn!("failed to run webview created hook: {_e}");
}
}
let webview = self.env.new_global_ref(webview)?;
ACTIVITY_PROXY
.lock()
.unwrap()
.get_mut(&activity_id)
.unwrap()
.webview
.replace(webview);
}
WebViewMessage::Eval(script, callback) => {
if let Some(webview) = get_webview(activity_id) {
let id = EVAL_ID_GENERATOR.next() as i32;
#[cfg(feature = "tracing")]
let span = std::sync::Mutex::new(Some(SendEnteredSpan(
tracing::debug_span!("wry::eval").entered(),
)));
EVAL_CALLBACKS
.get_or_init(Default::default)
.lock()
.unwrap()
.insert(
id,
Box::new(move |result| {
#[cfg(feature = "tracing")]
span.lock().unwrap().take();
if let Some(callback) = &callback {
callback(result);
}
}),
);
let s = self.env.new_string(script)?;
self.env.call_method(
webview.as_obj(),
"evalScript",
"(ILjava/lang/String;)V",
&[id.into(), (&s).into()],
)?;
}
}
WebViewMessage::SetBackgroundColor(background_color) => {
if let Some(webview) = get_webview(activity_id) {
set_background_color(&mut self.env, webview.as_obj(), background_color)?;
}
}
WebViewMessage::GetWebViewVersion(tx) => {
if let Some(activity) = activity_proxy(activity_id).map(|p| p.activity.clone()) {
match self
.env
.call_method(activity, "getVersion", "()Ljava/lang/String;", &[])
.and_then(|v| v.l())
.and_then(|s| {
let s = JString::from(s);
self
.env
.get_string(&s)
.map(|v| v.to_string_lossy().to_string())
}) {
Ok(version) => {
tx.send(Ok(version)).unwrap();
}
Err(e) => tx.send(Err(e.into())).unwrap(),
}
} else {
tx.send(Err(Error::ActivityNotFound)).unwrap();
}
}
WebViewMessage::GetUrl(tx) => {
if let Some(webview) = get_webview(activity_id) {
let url = self
.env
.call_method(webview.as_obj(), "getUrl", "()Ljava/lang/String;", &[])
.and_then(|v| v.l())
.and_then(|s| {
let s = JString::from(s);
self
.env
.get_string(&s)
.map(|v| v.to_string_lossy().to_string())
})
.unwrap_or_default();
tx.send(url).unwrap()
}
}
WebViewMessage::Jni(f) => {
match activity_proxy(activity_id).map(|p| (p.activity.clone(), p.webview.clone())) {
Some((activity, Some(webview))) => {
f(&mut self.env, &activity, webview.as_obj());
}
Some((activity, None)) => {
f(&mut self.env, &activity, &JObject::null());
}
_ => {
f(&mut self.env, &JObject::null(), &JObject::null());
}
}
}
WebViewMessage::LoadUrl(url, headers) => {
if let Some(webview) = get_webview(activity_id) {
let url = self.env.new_string(url)?;
load_url(&mut self.env, webview.as_obj(), &url, headers, false)?;
}
}
WebViewMessage::ClearAllBrowsingData => {
if let Some(webview) = get_webview(activity_id) {
self
.env
.call_method(webview, "clearAllBrowsingData", "()V", &[])?;
}
}
WebViewMessage::LoadHtml(html) => {
if let Some(webview) = get_webview(activity_id) {
let html = self.env.new_string(html)?;
load_html(&mut self.env, webview.as_obj(), &html)?;
}
}
WebViewMessage::Reload => {
if let Some(webview) = get_webview(activity_id) {
reload(&mut self.env, webview.as_obj())?;
}
}
WebViewMessage::GetCookies(tx, url) => {
if let Some(webview) = get_webview(activity_id) {
let url = self.env.new_string(url)?;
let cookies = self
.env
.call_method(
webview,
"getCookies",
"(Ljava/lang/String;)Ljava/lang/String;",
&[(&url).into()],
)
.and_then(|v| v.l())
.and_then(|s| {
let s = JString::from(s);
self
.env
.get_string(&s)
.map(|v| v.to_string_lossy().to_string())
})
.unwrap_or_default();
tx.send(
cookies
.split("; ")
.flat_map(|c| cookie::Cookie::parse(c.to_string()))
.collect(),
)
.unwrap();
}
}
WebViewMessage::OnDestroy {
activity_id,
webview_id,
is_changing_configurations,
} => {
// keep our webview references (callbacks etc) alive if the activity is going to be recreated due to configuration changes
// e.g. rotation, multi-window mode change, etc
if !is_changing_configurations {
super::destroy_webview(activity_id, &webview_id);
remove_activity_proxy(activity_id);
}
}
}
}
Ok(())
}
}
fn load_url<'a>(
env: &mut JNIEnv<'a>,
webview: &JObject<'a>,
url: &JString<'a>,
headers: Option<http::HeaderMap>,
main_thread: bool,
) -> JniResult<()> {
let function = if main_thread {
"loadUrlMainThread"
} else {
"loadUrl"
};
if let Some(headers) = headers {
let obj = env.new_object("java/util/HashMap", "()V", &[])?;
let headers_map = {
let headers_map = JMap::from_env(env, &obj)?;
for (name, value) in headers.iter() {
let key = env.new_string(name)?;
let value = env.new_string(value.to_str().unwrap_or_default())?;
headers_map.put(env, &key, &value)?;
}
headers_map
};
env.call_method(
webview,
function,
"(Ljava/lang/String;Ljava/util/Map;)V",
&[url.into(), (&headers_map).into()],
)?;
} else {
env.call_method(webview, function, "(Ljava/lang/String;)V", &[url.into()])?;
}
Ok(())
}
fn load_html<'a>(env: &mut JNIEnv<'a>, webview: &JObject<'a>, html: &JString<'a>) -> JniResult<()> {
env.call_method(
webview,
"loadHTMLMainThread",
"(Ljava/lang/String;)V",
&[html.into()],
)?;
Ok(())
}
fn reload<'a>(env: &mut JNIEnv<'a>, webview: &JObject<'a>) -> JniResult<()> {
env.call_method(webview, "reload", "()V", &[])?;
Ok(())
}
fn set_background_color<'a>(
env: &mut JNIEnv<'a>,
webview: &JObject<'a>,
(r, g, b, a): RGBA,
) -> JniResult<()> {
let color = (a as i32) << 24 | (r as i32) << 16 | (g as i32) << 8 | (b as i32);
env.call_method(webview, "setBackgroundColor", "(I)V", &[color.into()])?;
Ok(())
}
pub(crate) enum WebViewMessage {
CreateWebView(CreateWebViewAttributes),
Eval(String, Option<EvalCallback>),
SetBackgroundColor(RGBA),
GetWebViewVersion(Sender<Result<String, Error>>),
GetUrl(Sender<String>),
GetCookies(Sender<Vec<cookie::Cookie<'static>>>, String),
Jni(Box<dyn FnOnce(&mut JNIEnv, &JObject, &JObject) + Send>),
LoadUrl(String, Option<http::HeaderMap>),
LoadHtml(String),
Reload,
ClearAllBrowsingData,
OnDestroy {
activity_id: ActivityId,
webview_id: WebviewId,
is_changing_configurations: bool,
},
}
#[derive(Clone)]
pub(crate) struct CreateWebViewAttributes {
pub id: String,
pub url: Option<String>,
pub html: Option<String>,
#[cfg(any(debug_assertions, feature = "devtools"))]
pub devtools: bool,
pub transparent: bool,
pub background_color: Option<RGBA>,
pub headers: Option<http::HeaderMap>,
pub autoplay: bool,
pub on_webview_created:
Option<Arc<dyn Fn(super::Context) -> JniResult<()> + Send + Sync + 'static>>,
pub user_agent: Option<String>,
pub initialization_scripts: Vec<InitializationScript>,
pub javascript_disabled: bool,
}
// SAFETY: only use this when you are sure the span will be dropped on the same thread it was entered
#[cfg(feature = "tracing")]
struct SendEnteredSpan(tracing::span::EnteredSpan);
#[cfg(feature = "tracing")]
unsafe impl Send for SendEnteredSpan {}
+530
View File
@@ -0,0 +1,530 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use super::{PageLoadEvent, WebViewAttributes, RGBA};
use crate::{
custom_protocol_workaround, inject_initialization_scripts::inject_scripts_into_html, Error,
RequestAsyncResponder, Result,
};
use crossbeam_channel::*;
use http::{Request, Response as HttpResponse};
use jni::{
errors::Result as JniResult,
objects::{GlobalRef, JClass, JObject},
JNIEnv,
};
use ndk::looper::ThreadLooper;
use once_cell::sync::{Lazy, OnceCell};
use raw_window_handle::HasWindowHandle;
use std::{
borrow::Cow,
collections::HashMap,
sync::{mpsc::channel, Mutex},
time::Duration,
};
pub(crate) mod binding;
mod main_pipe;
use main_pipe::{
activity_id_for_window_manager, first_activity_id, register_activity_proxy, ActivityId,
CreateWebViewAttributes, MainPipe, WebViewMessage,
};
use crate::util::Counter;
static COUNTER: Counter = Counter::new();
const MAIN_PIPE_TIMEOUT: Duration = Duration::from_secs(10);
pub struct Context<'a, 'b> {
pub env: &'a mut JNIEnv<'b>,
pub activity: &'a JObject<'b>,
pub webview: &'a JObject<'b>,
}
type WebviewId = String;
macro_rules! define_static_handlers {
($($key: ident, $var:ident = $type_name:ident);+ $(;)?) => {
$(static $var: Lazy<Mutex<HashMap<$key, $type_name>>> = Lazy::new(||Mutex::new(HashMap::new()));)*
};
($($var:ident = $type_name:ident { $($fields:ident:$types:ty),+ $(,)? });+ $(;)?) => {
$(
static $var: Lazy<Mutex<HashMap<WebviewId, $type_name>>> = Lazy::new(||Mutex::new(HashMap::new()));
pub struct $type_name {
$($fields: $types,)*
}
impl $type_name {
pub fn new($($fields: $types,)*) -> Self {
Self {
$($fields,)*
}
}
}
unsafe impl Send for $type_name {}
unsafe impl Sync for $type_name {})*
};
}
define_static_handlers! {
IPC = UnsafeIpc { handler: Box<dyn Fn(Request<String>)> };
REQUEST_HANDLER = UnsafeRequestHandler { handler: Box<dyn Fn(&str, Request<Vec<u8>>, bool) -> Option<HttpResponse<Cow<'static, [u8]>>>> };
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)> };
}
define_static_handlers! {
WebviewId, WITH_ASSET_LOADER = bool;
WebviewId, ASSET_LOADER_DOMAIN = String;
ActivityId, WEBVIEW_ATTRIBUTES = CreateWebViewAttributes;
}
pub(crate) static PACKAGE: OnceCell<String> = OnceCell::new();
type EvalCallback = Box<dyn Fn(String) + Send + 'static>;
pub static EVAL_ID_GENERATOR: Counter = Counter::new();
pub static EVAL_CALLBACKS: OnceCell<Mutex<HashMap<i32, EvalCallback>>> = OnceCell::new();
pub fn destroy_webview(activity_id: ActivityId, webview_id: &WebviewId) {
WEBVIEW_ATTRIBUTES.lock().unwrap().remove(&activity_id);
IPC.lock().unwrap().remove(webview_id);
REQUEST_HANDLER.lock().unwrap().remove(webview_id);
TITLE_CHANGE_HANDLER.lock().unwrap().remove(webview_id);
URL_LOADING_OVERRIDE.lock().unwrap().remove(webview_id);
ON_LOAD_HANDLER.lock().unwrap().remove(webview_id);
WITH_ASSET_LOADER.lock().unwrap().remove(webview_id);
ASSET_LOADER_DOMAIN.lock().unwrap().remove(webview_id);
}
/// Sets up the necessary logic for wry to be able to create the webviews later.
///
/// This function must be run on the thread where the [`JNIEnv`] is registered and the looper is local,
/// hence the requirement for a [`ThreadLooper`].
pub unsafe fn android_setup(
package: &str,
mut env: JNIEnv,
_looper: &ThreadLooper,
activity: GlobalRef,
) {
PACKAGE.get_or_init(move || package.to_string());
let vm = env.get_java_vm().unwrap();
let activity_id = env
.call_method(activity.as_obj(), "getId", "()I", &[])
.unwrap()
.i()
.unwrap();
let window_manager = env
.call_method(
&activity,
"getWindowManager",
"()Landroid/view/WindowManager;",
&[],
)
.unwrap()
.l()
.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);
if let Some(webview_attributes) = WEBVIEW_ATTRIBUTES.lock().unwrap().get(&activity_id) {
MainPipe::send(
activity_id,
WebViewMessage::CreateWebView(webview_attributes.clone()),
);
}
}
pub(crate) struct InnerWebView {
id: String,
pub activity_id: ActivityId,
}
impl InnerWebView {
pub fn new_as_child(
window: &impl HasWindowHandle,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
) -> Result<Self> {
Self::new(window, attributes, pl_attrs)
}
pub fn new(
window: &impl HasWindowHandle,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
) -> Result<Self> {
let window_manager = match window.window_handle()?.as_raw() {
raw_window_handle::RawWindowHandle::AndroidNdk(window_manager) => {
window_manager.a_native_window
}
_ => return Err(Error::UnsupportedWindowHandle),
};
let window_manager = unsafe { JObject::from_raw(window_manager.as_ptr().cast()) };
let activity_id =
activity_id_for_window_manager(window_manager).expect("no available activity");
let WebViewAttributes {
url,
html,
initialization_scripts,
ipc_handler,
#[cfg(any(debug_assertions, feature = "devtools"))]
devtools,
custom_protocols,
background_color,
transparent,
headers,
autoplay,
user_agent,
javascript_disabled,
..
} = attributes;
let super::PlatformSpecificWebViewAttributes {
on_webview_created,
with_asset_loader,
asset_loader_domain,
https_scheme,
} = pl_attrs;
let http_or_https = if https_scheme { "https" } else { "http" };
let url = if let Some(mut url) = url {
if let Some((protocol, _)) = url.split_once("://") {
if custom_protocols.contains_key(protocol) {
url = custom_protocol_workaround::apply_uri_work_around(&url, http_or_https, protocol)
}
}
Some(url)
} else {
None
};
let id = attributes
.id
.map(|id| id.to_string())
.unwrap_or_else(|| COUNTER.next().to_string());
WITH_ASSET_LOADER
.lock()
.unwrap()
.insert(id.clone(), with_asset_loader);
if let Some(domain) = asset_loader_domain {
ASSET_LOADER_DOMAIN
.lock()
.unwrap()
.insert(id.clone(), domain);
}
let initialization_scripts_ = initialization_scripts.clone();
REQUEST_HANDLER
.lock()
.unwrap()
.insert(
id.clone(),
UnsafeRequestHandler::new(Box::new(
move |webview_id: &str, mut request, is_document_start_script_enabled| {
let uri = request.uri().to_string();
if let Some((custom_protocol, custom_protocol_handler)) =
custom_protocols.iter().find(|(protocol, _)| {
custom_protocol_workaround::is_work_around_uri(&uri, http_or_https, protocol)
})
{
let uri_res = custom_protocol_workaround::revert_uri_work_around(
&uri,
http_or_https,
custom_protocol,
)
.parse();
if let Ok(uri) = uri_res {
*request.uri_mut() = uri;
}
let (tx, rx) = channel();
let initialization_scripts = initialization_scripts_.clone();
let responder: Box<dyn FnOnce(HttpResponse<Cow<'static, [u8]>>)> =
Box::new(move |mut response| {
if !is_document_start_script_enabled {
#[cfg(feature = "tracing")]
tracing::info!("`addDocumentStartJavaScript` is not supported; injecting initialization scripts via custom protocol handler");
response = inject_scripts_into_html(response, &initialization_scripts);
}
let _ = tx.send(response);
});
(custom_protocol_handler)(webview_id, request, RequestAsyncResponder { responder });
// 3x the timeout while we monitor https://github.com/tauri-apps/wry/issues/1551
// TODO: Remove timeout
return rx.recv_timeout(MAIN_PIPE_TIMEOUT * 3).inspect_err(|e| {eprintln!("custom protocol timed out: {e}");}).ok();
}
None
},
)));
if let Some(i) = ipc_handler {
IPC
.lock()
.unwrap()
.insert(id.clone(), UnsafeIpc::new(Box::new(i)));
}
if let Some(i) = attributes.document_title_changed_handler {
TITLE_CHANGE_HANDLER
.lock()
.unwrap()
.insert(id.clone(), UnsafeTitleHandler::new(i));
}
if let Some(i) = attributes.navigation_handler {
URL_LOADING_OVERRIDE
.lock()
.unwrap()
.insert(id.clone(), UnsafeUrlLoadingOverride::new(i));
}
if let Some(h) = attributes.on_page_load_handler {
ON_LOAD_HANDLER
.lock()
.unwrap()
.insert(id.clone(), UnsafeOnPageLoadHandler::new(h));
}
let attributes = CreateWebViewAttributes {
id: id.clone(),
url,
html,
#[cfg(any(debug_assertions, feature = "devtools"))]
devtools,
background_color,
transparent,
headers,
on_webview_created,
autoplay,
user_agent,
initialization_scripts,
javascript_disabled,
};
WEBVIEW_ATTRIBUTES
.lock()
.unwrap()
.insert(activity_id, attributes.clone());
MainPipe::send(activity_id, WebViewMessage::CreateWebView(attributes));
Ok(Self { id, activity_id })
}
pub fn print(&self) -> crate::Result<()> {
Ok(())
}
pub fn id(&self) -> crate::WebViewId<'_> {
&self.id
}
pub fn url(&self) -> crate::Result<String> {
let (tx, rx) = bounded(1);
MainPipe::send(self.activity_id, WebViewMessage::GetUrl(tx));
rx.recv_timeout(MAIN_PIPE_TIMEOUT).map_err(Into::into)
}
pub fn eval(&self, js: &str, callback: Option<impl Fn(String) + Send + 'static>) -> Result<()> {
MainPipe::send(
self.activity_id,
WebViewMessage::Eval(
js.into(),
callback.map(|c| Box::new(c) as Box<dyn Fn(String) + Send + 'static>),
),
);
Ok(())
}
#[cfg(any(debug_assertions, feature = "devtools"))]
pub fn open_devtools(&self) {}
#[cfg(any(debug_assertions, feature = "devtools"))]
pub fn close_devtools(&self) {}
#[cfg(any(debug_assertions, feature = "devtools"))]
pub fn is_devtools_open(&self) -> bool {
false
}
pub fn zoom(&self, _scale_factor: f64) -> Result<()> {
Ok(())
}
pub fn set_background_color(&self, background_color: RGBA) -> Result<()> {
MainPipe::send(
self.activity_id,
WebViewMessage::SetBackgroundColor(background_color),
);
Ok(())
}
pub fn load_url(&self, url: &str) -> Result<()> {
MainPipe::send(
self.activity_id,
WebViewMessage::LoadUrl(url.to_string(), None),
);
Ok(())
}
pub fn load_url_with_headers(&self, url: &str, headers: http::HeaderMap) -> Result<()> {
MainPipe::send(
self.activity_id,
WebViewMessage::LoadUrl(url.to_string(), Some(headers)),
);
Ok(())
}
pub fn load_html(&self, html: &str) -> Result<()> {
MainPipe::send(self.activity_id, WebViewMessage::LoadHtml(html.to_string()));
Ok(())
}
pub fn reload(&self) -> Result<()> {
MainPipe::send(self.activity_id, WebViewMessage::Reload);
Ok(())
}
pub fn clear_all_browsing_data(&self) -> Result<()> {
MainPipe::send(self.activity_id, WebViewMessage::ClearAllBrowsingData);
Ok(())
}
pub fn cookies_for_url(&self, url: &str) -> Result<Vec<cookie::Cookie<'static>>> {
let (tx, rx) = bounded(1);
MainPipe::send(
self.activity_id,
WebViewMessage::GetCookies(tx, url.to_string()),
);
rx.recv_timeout(MAIN_PIPE_TIMEOUT).map_err(Into::into)
}
pub fn set_cookie(&self, #[allow(unused)] cookie: &cookie::Cookie<'_>) -> Result<()> {
// Unsupported
Ok(())
}
pub fn delete_cookie(&self, #[allow(unused)] cookie: &cookie::Cookie<'_>) -> Result<()> {
// Unsupported
Ok(())
}
pub fn cookies(&self) -> Result<Vec<cookie::Cookie<'static>>> {
Ok(Vec::new())
}
pub fn bounds(&self) -> Result<crate::Rect> {
Ok(crate::Rect::default())
}
pub fn set_bounds(&self, _bounds: crate::Rect) -> Result<()> {
// Unsupported
Ok(())
}
pub fn set_visible(&self, _visible: bool) -> Result<()> {
// Unsupported
Ok(())
}
pub fn focus(&self) -> Result<()> {
// Unsupported
Ok(())
}
pub fn focus_parent(&self) -> Result<()> {
// Unsupported
Ok(())
}
}
#[derive(Clone, Copy)]
pub struct JniHandle {
pub(crate) activity_id: ActivityId,
}
impl JniHandle {
/// Execute jni code on the thread of the webview.
/// Provided function will be provided with the jni evironment, Android activity and WebView
pub fn exec<F>(&self, func: F)
where
F: FnOnce(&mut JNIEnv, &JObject, &JObject) + Send + 'static,
{
MainPipe::send(self.activity_id, WebViewMessage::Jni(Box::new(func)));
}
}
pub fn platform_webview_version() -> Result<String> {
let (tx, rx) = bounded(1);
let activity_id = loop {
match first_activity_id() {
Some(id) => break id,
None => {
std::thread::sleep(Duration::from_millis(100));
}
}
};
MainPipe::send(activity_id, WebViewMessage::GetWebViewVersion(tx));
rx.recv_timeout(MAIN_PIPE_TIMEOUT)?
}
/// Finds a class in the project scope.
pub fn find_class<'a>(
env: &mut JNIEnv<'a>,
activity: &JObject<'_>,
name: String,
) -> JniResult<JClass<'a>> {
let class_name = env.new_string(name.replace('/', "."))?;
let my_class = env
.call_method(
activity,
"getAppClass",
"(Ljava/lang/String;)Ljava/lang/Class;",
&[(&class_name).into()],
)?
.l()?;
Ok(my_class.into())
}
/// Dispatch a closure to run on the Android context.
///
/// The closure takes the JNI env, the Android activity instance and the possibly null webview.
pub fn dispatch<F>(func: F)
where
F: FnOnce(&mut JNIEnv, &JObject, &JObject) + Send + 'static,
{
MainPipe::send(
first_activity_id().expect("no available activity"),
WebViewMessage::Jni(Box::new(func)),
);
}
+56
View File
@@ -0,0 +1,56 @@
//! - WebView2 supports non-standard protocols only on Windows 10+, so we have to use a workaround.
//! 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:
//!
//! 1. Use [`apply_uri_work_around`] to convert the URI we want to navigate to
//! 2. Intercept http(s) requests, test the request URI against [`is_work_around_uri`],
//! if it matches, we apply [`revert_uri_work_around`] to the URI and feed it to the custom protocol handler
/// If the URI is a work around URI for this protocol which starts with `{http_or_https}://{protocol}.`
pub fn is_work_around_uri(uri: &str, http_or_https: &str, protocol: &str) -> bool {
uri
.strip_prefix(http_or_https)
.and_then(|rest| rest.strip_prefix("://"))
.and_then(|rest| rest.strip_prefix(protocol))
.and_then(|rest| rest.strip_prefix("."))
.is_some()
}
/// Conveting `{protocol}://localhost/abc` to `{http_or_https}://{protocol}.localhost/abc`
pub fn apply_uri_work_around(uri: &str, http_or_https: &str, protocol: &str) -> String {
uri.replace(
&original_uri_prefix(protocol),
&work_around_uri_prefix(http_or_https, protocol),
)
}
/// Conveting `{http_or_https}://{protocol}.localhost/abc` back to `{protocol}://localhost/abc`
pub fn revert_uri_work_around(uri: &str, http_or_https: &str, protocol: &str) -> String {
uri.replace(
&work_around_uri_prefix(http_or_https, protocol),
&original_uri_prefix(protocol),
)
}
pub fn original_uri_prefix(protocol: &str) -> String {
format!("{protocol}://")
}
pub fn work_around_uri_prefix(http_or_https: &str, protocol: &str) -> String {
format!("{http_or_https}://{protocol}.")
}
#[cfg(test)]
mod tests {
use super::is_work_around_uri;
#[test]
fn checks_if_custom_protocol_uri() {
let scheme = "http";
let uri = "http://wry.localhost/path/to/page";
assert!(is_work_around_uri(uri, scheme, "wry"));
assert!(!is_work_around_uri(uri, scheme, "asset"));
}
}
+80
View File
@@ -0,0 +1,80 @@
/// Convenient type alias of Result type for wry.
pub type Result<T> = std::result::Result<T, Error>;
/// Errors returned by wry.
#[non_exhaustive]
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[cfg(gtk)]
#[error(transparent)]
GlibError(#[from] gtk::glib::Error),
#[cfg(gtk)]
#[error(transparent)]
GlibBoolError(#[from] gtk::glib::BoolError),
#[cfg(gtk)]
#[error("Fail to fetch security manager")]
MissingManager,
#[cfg(gtk)]
#[error("Couldn't find X11 Display")]
X11DisplayNotFound,
#[cfg(all(gtk, feature = "x11"))]
#[error(transparent)]
XlibError(#[from] x11_dl::error::OpenError),
#[error("Failed to initialize the script")]
InitScriptError,
#[error("Bad RPC request: {0} ((1))")]
RpcScriptError(String, String),
#[error(transparent)]
NulError(#[from] std::ffi::NulError),
#[error(transparent)]
ReceiverError(#[from] std::sync::mpsc::RecvError),
#[cfg(target_os = "android")]
#[error(transparent)]
ReceiverTimeoutError(#[from] crossbeam_channel::RecvTimeoutError),
#[error(transparent)]
SenderError(#[from] std::sync::mpsc::SendError<String>),
#[error("Failed to send the message")]
MessageSender,
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[cfg(target_os = "windows")]
#[error("WebView2 error: {0}")]
WebView2Error(webview2_com::Error),
#[error(transparent)]
HttpError(#[from] http::Error),
#[error("Infallible error, something went really wrong: {0}")]
Infallible(#[from] std::convert::Infallible),
#[cfg(target_os = "android")]
#[error(transparent)]
JniError(#[from] jni::errors::Error),
#[error("Failed to create proxy endpoint")]
ProxyEndpointCreationFailed,
#[error(transparent)]
WindowHandleError(#[from] raw_window_handle::HandleError),
#[error("the window handle kind is not supported")]
UnsupportedWindowHandle,
#[error(transparent)]
Utf8Error(#[from] std::str::Utf8Error),
#[cfg(target_os = "android")]
#[error(transparent)]
CrossBeamRecvError(#[from] crossbeam_channel::RecvError),
#[error("not on the main thread")]
NotMainThread,
#[error("Custom protocol task is invalid.")]
CustomProtocolTaskInvalid,
#[error("Failed to register URL scheme: {0}, could be due to invalid URL scheme or the scheme is already registered.")]
UrlSchemeRegisterError(String),
#[error("Duplicate custom protocol '{0}' registered on the WebViewBuilder")]
DuplicateCustomProtocol(String),
#[error("Duplicate custom protocol '{0}' registered on the same web context on Linux")]
ContextDuplicateCustomProtocol(String),
#[error(transparent)]
#[cfg(any(target_os = "macos", target_os = "ios"))]
UrlParse(#[from] url::ParseError),
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[error("data store is currently opened")]
DataStoreInUse,
#[cfg(target_os = "android")]
#[error("Activity not found")]
ActivityNotFound,
}
@@ -0,0 +1,225 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! This is an internal implementation detail used by the Android backend to inject
//! initialization scripts when `addDocumentStartJavaScript` is not supported.
use base64::{prelude::BASE64_STANDARD, Engine};
use dom_query::Document;
use http::{
header::{HeaderValue, CONTENT_SECURITY_POLICY, CONTENT_TYPE},
Response as HttpResponse,
};
use sha2::{Digest, Sha256};
use std::borrow::Cow;
use crate::InitializationScript;
pub fn inject_scripts_into_html(
mut response: HttpResponse<Cow<'static, [u8]>>,
scripts: &[InitializationScript],
) -> HttpResponse<Cow<'static, [u8]>> {
if scripts.is_empty() {
return response;
}
let should_inject_scripts = response
.headers()
.get(CONTENT_TYPE)
// Content-Type must begin with the media type, but is case-insensitive.
// It may also be followed by any number of semicolon-delimited key value pairs.
// We don't care about these here.
// source: https://httpwg.org/specs/rfc9110.html#rfc.section.8.3.1
.and_then(|content_type| content_type.to_str().ok())
.map(|content_type_str| content_type_str.to_lowercase().starts_with("text/html"))
.unwrap_or_default();
if !should_inject_scripts {
return response;
}
let document = Document::from(String::from_utf8_lossy(response.body()).as_ref());
let csp = response.headers_mut().get_mut(CONTENT_SECURITY_POLICY);
// Get or create head element
let head = document.head().unwrap_or_else(|| {
let html = document.html_root();
let head = document.tree.new_element("head");
html.prepend_child(&head);
head
});
// Iterate in reverse order since we are prepending each script to the head tag
let mut hashes = Vec::new();
for script in scripts.iter().rev().map(|s| &s.script) {
let script_tag = document.tree.new_element("script");
script_tag.set_text(script.as_str());
head.prepend_child(&script_tag);
if csp.is_some() {
hashes.push(hash_script(script));
}
}
if let Some(csp) = csp {
let csp_string = csp.to_str().unwrap().to_string();
let csp_string = if csp_string.contains("script-src") {
csp_string.replace("script-src", &format!("script-src {}", hashes.join(" ")))
} else {
format!("{csp_string} script-src {}", hashes.join(" "))
};
*csp = HeaderValue::from_str(&csp_string).unwrap();
}
*response.body_mut() = Cow::Owned(document.html().as_bytes().to_vec());
response
}
fn hash_script(script: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(script);
let hash = hasher.finalize();
format!("'sha256-{}'", BASE64_STANDARD.encode(hash))
}
#[cfg(test)]
mod tests {
use super::*;
use http::StatusCode;
#[test]
fn test_no_scripts_returns_original_response() {
let body = "<html><head></head><body>Test</body></html>";
let result = run(body, "text/html", vec![]);
assert_eq!(result, body);
}
#[test]
fn test_non_html_response_not_modified() {
let body = r#"{"key": "value"}"#;
let scripts = vec!["console.log('test');".to_string()];
let result = run(body, "application/json", scripts);
assert_eq!(result, body);
}
#[test]
fn test_inject_single_script() {
let body = "<html><head></head><body>Content</body></html>";
let scripts = vec!["console.log('injected');".to_string()];
let result = run(body, "text/html", scripts);
assert_eq!(
result,
"<html><head><script>console.log('injected');</script></head><body>Content</body></html>"
);
}
#[test]
fn test_inject_multiple_scripts() {
let body = "<html><head></head><body>Content</body></html>";
let scripts = vec![
"var first = 1;".to_owned(),
"let second = 2;".to_owned(),
"const third = 3;".to_owned(),
"window.test = () => console.log('test');".to_owned(),
];
let result = run(body, "text/html", scripts);
assert_eq!(
result,
"<html><head><script>var first = 1;</script><script>let second = 2;</script><script>const third = 3;</script><script>window.test = () => console.log('test');</script></head><body>Content</body></html>"
);
}
#[test]
fn test_inject_script_creates_head_if_missing() {
let body = "<html><body>Content</body></html>";
let scripts = vec!["console.log('test');".to_string()];
let result = run(body, "text/html", scripts);
assert_eq!(
result,
"<html><head><script>console.log('test');</script></head><body>Content</body></html>"
);
}
#[test]
fn test_inject_script_creates_html_structure_if_missing() {
let body = "Just some text";
let scripts = vec!["console.log('test');".to_string()];
let result = run(body, "text/html", scripts);
assert_eq!(
result,
"<html><head><script>console.log('test');</script></head><body>Just some text</body></html>"
);
}
#[test]
fn test_csp_header_updated_with_script_hashes() {
let body = "<html><head></head><body>Content</body></html>";
let mut response = create_response(body, "text/html");
response.headers_mut().insert(
CONTENT_SECURITY_POLICY,
HeaderValue::from_static("default-src 'self'"),
);
let script_code = "console.log('test');";
let scripts = vec![script_code.to_string()];
let scripts: Vec<InitializationScript> = scripts
.into_iter()
.map(|script| InitializationScript {
script,
for_main_frame_only: true,
})
.collect();
let result = inject_scripts_into_html(response, &scripts);
let result_body = String::from_utf8_lossy(result.body()).to_string();
let csp = result.headers().get(CONTENT_SECURITY_POLICY).unwrap();
let csp_str = csp.to_str().unwrap();
assert_eq!(
result_body,
"<html><head><script>console.log('test');</script></head><body>Content</body></html>"
);
assert_eq!(
csp_str,
"default-src 'self' script-src 'sha256-3x8DE279hr8o/Aq0dEdH4WApIwn5rbRKhugPzn6Bofw='"
);
}
fn create_response(body: &str, content_type: &'static str) -> HttpResponse<Cow<'static, [u8]>> {
let mut response = HttpResponse::builder()
.status(StatusCode::OK)
.body(Cow::Owned(body.as_bytes().to_vec()))
.unwrap();
response
.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
response
}
/// Helper function to create a response, inject scripts, and return the body as a string
fn run(body: &str, content_type: &'static str, scripts: Vec<String>) -> String {
let response = create_response(body, content_type);
let scripts: Vec<InitializationScript> = scripts
.into_iter()
.map(|script| InitializationScript {
script,
for_main_frame_only: true,
})
.collect();
let result = inject_scripts_into_html(response, &scripts);
String::from_utf8_lossy(result.body()).to_string()
}
}
+2516
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
#[derive(Debug, Clone)]
pub struct ProxyEndpoint {
/// Proxy server host (e.g. 192.168.0.100, localhost, example.com, etc.)
pub host: String,
/// Proxy server port (e.g. 1080, 3128, etc.)
pub port: String,
}
#[derive(Debug, Clone)]
pub enum ProxyConfig {
/// Connect to proxy server via HTTP CONNECT
Http(ProxyEndpoint),
/// Connect to proxy server via SOCKSv5
Socks5(ProxyEndpoint),
}
+13
View File
@@ -0,0 +1,13 @@
use std::sync::atomic::{AtomicU32, Ordering};
pub struct Counter(AtomicU32);
impl Counter {
pub const fn new() -> Self {
Self(AtomicU32::new(1))
}
pub fn next(&self) -> u32 {
self.0.fetch_add(1, Ordering::Relaxed)
}
}
+113
View File
@@ -0,0 +1,113 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#[cfg(gtk)]
use crate::webkitgtk::WebContextImpl;
use std::{
collections::HashSet,
path::{Path, PathBuf},
};
/// A context that is shared between multiple [`WebView`]s.
///
/// A browser would have a context for all the normal tabs and a different context for all the
/// private/incognito tabs.
///
/// # Warning
///
/// If [`WebView`] is created by a WebContext. Dropping `WebContext` will cause [`WebView`] lose
/// some actions like custom protocol on Mac. Please keep both instances when you still wish to
/// interact with them.
///
/// [`WebView`]: crate::WebView
#[derive(Debug)]
pub struct WebContext {
data_directory: Option<PathBuf>,
#[allow(dead_code)] // It's not needed on Windows and macOS.
pub(crate) os: WebContextImpl,
#[allow(dead_code)] // It's not needed on Windows and macOS.
pub(crate) custom_protocols: HashSet<String>,
}
impl WebContext {
/// Create a new [`WebContext`].
///
/// - `data_directory`: Whether the WebView window should have a custom user data path.
/// This is useful in Windows when a bundled application can't have the webview data inside `Program Files`.
///
/// ## Platform-specific:
///
/// - **Windows**: Webview instances with different `CoreWebView2EnvironmentOptions` must have different `data_directory`s [^1]
///
/// [^1]: <https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2environment.createcorewebview2controllerasync?view=webview2-dotnet-1.0.3719.77#:~:text=WebView%20creation%20fails%20if%20a%20running%20instance%20using%20the%20same%20user%20data%20folder%20exists%2C%20and%20the%20Environment%20objects%20have%20different%20CoreWebView2EnvironmentOptions.>
pub fn new(data_directory: Option<PathBuf>) -> Self {
Self {
os: WebContextImpl::new(data_directory.as_deref()),
data_directory,
custom_protocols: Default::default(),
}
}
#[cfg(gtk)]
pub(crate) fn new_ephemeral() -> Self {
Self {
os: WebContextImpl::new_ephemeral(),
data_directory: None,
custom_protocols: Default::default(),
}
}
/// A reference to the data directory the context was created with.
pub fn data_directory(&self) -> Option<&Path> {
self.data_directory.as_deref()
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
))]
pub(crate) fn register_custom_protocol(&mut self, name: String) -> Result<(), crate::Error> {
if self.is_custom_protocol_registered(&name) {
return Err(crate::Error::ContextDuplicateCustomProtocol(name));
}
self.custom_protocols.insert(name);
Ok(())
}
/// Check if a custom protocol has been registered on this context.
pub fn is_custom_protocol_registered(&self, name: &str) -> bool {
self.custom_protocols.contains(name)
}
/// Set if this context allows automation.
///
/// **Note:** This is currently only enforced on Linux, and has the stipulation that
/// only 1 context allows automation at a time.
pub fn set_allows_automation(&mut self, flag: bool) {
self.os.set_allows_automation(flag);
}
}
impl Default for WebContext {
fn default() -> Self {
Self::new(None)
}
}
#[cfg(not(gtk))]
#[derive(Debug)]
pub(crate) struct WebContextImpl;
#[cfg(not(gtk))]
impl WebContextImpl {
fn new(_: Option<&Path>) -> Self {
Self
}
fn set_allows_automation(&mut self, _flag: bool) {}
}
+143
View File
@@ -0,0 +1,143 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
cell::{Cell, UnsafeCell},
path::PathBuf,
rc::Rc,
};
use gtk::{glib::GString, prelude::*};
use webkit2gtk::WebView;
use crate::DragDropEvent;
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)]
enum DragControllerState {
Entered,
Leaving,
Left,
}
struct DragDropController {
paths: UnsafeCell<Option<Vec<PathBuf>>>,
state: Cell<DragControllerState>,
position: Cell<(i32, i32)>,
handler: Box<dyn Fn(DragDropEvent) -> bool>,
}
impl DragDropController {
fn new(handler: Box<dyn Fn(DragDropEvent) -> bool>) -> Self {
Self {
handler,
paths: UnsafeCell::new(None),
state: Cell::new(DragControllerState::Left),
position: Cell::new((0, 0)),
}
}
fn store_paths(&self, paths: Vec<PathBuf>) {
unsafe { *self.paths.get() = Some(paths) };
}
fn take_paths(&self) -> Option<Vec<PathBuf>> {
unsafe { &mut *self.paths.get() }.take()
}
fn store_position(&self, position: (i32, i32)) {
self.position.replace(position);
}
fn enter(&self) {
self.state.set(DragControllerState::Entered);
}
fn leaving(&self) {
self.state.set(DragControllerState::Leaving);
}
fn leave(&self) {
self.state.set(DragControllerState::Left);
}
fn state(&self) -> DragControllerState {
self.state.get()
}
fn call(&self, event: DragDropEvent) -> bool {
(self.handler)(event)
}
}
pub(crate) fn connect_drag_event(webview: &WebView, handler: Box<dyn Fn(DragDropEvent) -> bool>) {
let controller = Rc::new(DragDropController::new(handler));
{
let controller = controller.clone();
webview.connect_drag_data_received(move |_, _, _, _, data, info, _| {
if info == 2 {
let uris = data.uris();
let paths = uris.iter().map(path_buf_from_uri).collect::<Vec<_>>();
controller.enter();
controller.call(DragDropEvent::Enter {
paths: paths.clone(),
position: controller.position.get(),
});
controller.store_paths(paths);
}
});
}
{
let controller = controller.clone();
webview.connect_drag_motion(move |_, _, x, y, _| {
if controller.state() == DragControllerState::Entered {
controller.call(DragDropEvent::Over { position: (x, y) });
} else {
controller.store_position((x, y));
}
false
});
}
{
let controller = controller.clone();
webview.connect_drag_drop(move |_, ctx, x, y, time| {
if controller.state() == DragControllerState::Leaving {
if let Some(paths) = controller.take_paths() {
ctx.drop_finish(true, time);
controller.leave();
return controller.call(DragDropEvent::Drop {
paths,
position: (x, y),
});
}
}
false
});
}
webview.connect_drag_leave(move |_w, _, _| {
if controller.state() != DragControllerState::Left {
controller.leaving();
let controller = controller.clone();
gtk::glib::idle_add_local_once(move || {
if controller.state() == DragControllerState::Leaving {
controller.leave();
controller.call(DragDropEvent::Leave);
}
});
}
});
}
fn path_buf_from_uri(gstr: &GString) -> PathBuf {
let path = gstr.as_str();
let path = path.strip_prefix("file://").unwrap_or(path);
let path = percent_encoding::percent_decode(path.as_bytes())
.decode_utf8_lossy()
.to_string();
PathBuf::from(path)
}
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More