feat: 大屏桌面壳(Tauri 2)
|
After Width: | Height: | Size: 2.7 KiB |
@@ -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
|
||||
@@ -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" }
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 944 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 958 B |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
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>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
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>
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 641 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 815 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
@@ -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");
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":1}
|
||||
@@ -0,0 +1,4 @@
|
||||
# [build]
|
||||
# target = "aarch64-linux-android"
|
||||
[target.x86_64-apple-darwin]
|
||||
rustflags = ["-C", "link-arg=-mmacosx-version-min=10.12"]
|
||||
@@ -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/
|
||||
@@ -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
|
||||
@@ -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)",
|
||||
]
|
||||
@@ -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)"]
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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].
|
||||
@@ -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://crates.io/crates/wry) [](https://docs.rs/wry/)
|
||||
[](https://opencollective.com/tauri)
|
||||
[](https://discord.gg/SpmNs4S)
|
||||
[](https://tauri.app)
|
||||
[](https://good-labs.github.io/greater-good-affirmation)
|
||||
[](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
|
||||
@@ -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.
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": ["config:recommended"],
|
||||
"rangeStrategy": "replace",
|
||||
"packageRules": [
|
||||
{
|
||||
"semanticCommitType": "chore",
|
||||
"matchPackageNames": ["*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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}}
|
||||
}
|
||||
@@ -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>(...);
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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)),
|
||||
);
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||