Files
web2app/.github/scripts/patch-android-system-bars.mjs
shumengya e7e6f4b84f fix(android): enable camera permission for WebView getUserMedia
Declare CAMERA in AndroidManifest so RustWebChromeClient can show the system permission dialog. Inject Permissions-Policy meta for camera access in the Tauri shell.
2026-07-03 20:28:27 +08:00

228 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, "../..");
const androidRoot = path.join(root, "template", "src-tauri", "gen", "android");
function walk(dir, matcher) {
const hits = [];
if (!fs.existsSync(dir)) return hits;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
hits.push(...walk(full, matcher));
} else if (matcher(full)) {
hits.push(full);
}
}
return hits;
}
function patchMainActivity(filePath) {
const original = fs.readFileSync(filePath, "utf8");
const packageMatch = original.match(/^package\s+([^\s]+)/m);
if (!packageMatch) {
console.warn(`Skip MainActivity (no package): ${filePath}`);
return;
}
const pkg = packageMatch[1];
const content = `package ${pkg}
import android.os.Bundle
import android.webkit.CookieManager
import android.webkit.WebView
import androidx.core.view.WindowCompat
class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 内容不延伸到状态栏 / 导航栏区域
WindowCompat.setDecorFitsSystemWindows(window, true)
CookieManager.getInstance().setAcceptCookie(true)
}
override fun onWebViewCreate(webView: WebView) {
super.onWebViewCreate(webView)
// Android WebView 默认拦截跨站 Cookie桌面壳请求远程 API 时需放行
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true)
}
}
`;
fs.writeFileSync(filePath, content);
console.log(`Patched MainActivity: ${path.relative(root, filePath)}`);
}
function ensureThemeItems(xml) {
let next = xml;
const items = [
`<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`,
`<item name="android:statusBarColor">?android:colorBackground</item>`,
`<item name="android:navigationBarColor">?android:colorBackground</item>`,
];
for (const item of items) {
const name = item.match(/name="([^"]+)"/)?.[1];
if (!name) continue;
if (next.includes(`name="${name}"`)) {
next = next.replace(
new RegExp(`<item name="${name}">[^<]*</item>`, "g"),
item,
);
} else {
next = next.replace(/<\/style>/, ` ${item}\n </style>`);
}
}
return next;
}
function patchThemes(filePath) {
const original = fs.readFileSync(filePath, "utf8");
const patched = ensureThemeItems(original);
if (patched !== original) {
fs.writeFileSync(filePath, patched);
console.log(`Patched theme: ${path.relative(root, filePath)}`);
}
}
if (!fs.existsSync(androidRoot)) {
console.error(`Android project not found: ${androidRoot}`);
console.error("Run tauri android init before patch-android-system-bars.mjs");
process.exit(1);
}
for (const file of walk(androidRoot, (p) => p.endsWith("MainActivity.kt"))) {
patchMainActivity(file);
}
for (const file of walk(androidRoot, (p) => {
const normalized = p.replace(/\\/g, "/");
return (
normalized.endsWith("values/themes.xml") ||
normalized.endsWith("values-night/themes.xml")
);
})) {
patchThemes(file);
}
const NETWORK_SECURITY_XML = `<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
`;
function ensureNetworkSecurityXml(androidRoot) {
const resDir = path.join(androidRoot, "app", "src", "main", "res");
if (!fs.existsSync(resDir)) {
console.warn(
`No Android res directory found for network_security_config.xml: ${resDir}`,
);
return;
}
const xmlDir = path.join(resDir, "xml");
fs.mkdirSync(xmlDir, { recursive: true });
const target = path.join(xmlDir, "network_security_config.xml");
fs.writeFileSync(target, NETWORK_SECURITY_XML);
console.log(`Wrote ${path.relative(root, target)}`);
}
const ANDROID_PERMISSIONS = [
"android.permission.INTERNET",
"android.permission.CAMERA",
];
function ensureUsesPermission(xml, permission) {
if (xml.includes(permission)) return xml;
const tag = ` <uses-permission android:name="${permission}" />\n`;
if (xml.includes("<application")) {
return xml.replace(/<application/, `${tag}\n <application`);
}
return xml.replace(/<\/manifest>/, `${tag}</manifest>`);
}
function patchAndroidManifest(androidRoot) {
for (const file of walk(androidRoot, (p) => p.endsWith("AndroidManifest.xml"))) {
const normalized = file.replace(/\\/g, "/");
if (!normalized.includes("/src/main/")) continue;
let xml = fs.readFileSync(file, "utf8");
for (const permission of ANDROID_PERMISSIONS) {
xml = ensureUsesPermission(xml, permission);
}
if (!xml.includes("android.hardware.camera")) {
xml = xml.replace(
/<application/,
' <uses-feature android:name="android.hardware.camera" android:required="false" />\n\n <application',
);
}
xml = xml.replace(
/android:usesCleartextTraffic="\$\{usesCleartextTraffic\}"/g,
'android:usesCleartextTraffic="true"',
);
xml = xml.replace(
/android:usesCleartextTraffic="false"/g,
'android:usesCleartextTraffic="true"',
);
if (!xml.includes("usesCleartextTraffic")) {
xml = xml.replace(
/<application([^>]*)>/,
'<application$1 android:usesCleartextTraffic="true">',
);
}
if (!xml.includes("networkSecurityConfig")) {
xml = xml.replace(
/<application([^>]*)>/,
'<application$1 android:networkSecurityConfig="@xml/network_security_config">',
);
}
fs.writeFileSync(file, xml);
console.log(`Patched AndroidManifest: ${path.relative(root, file)}`);
}
}
function patchGradleCleartext(androidRoot) {
for (const file of walk(androidRoot, (p) => p.endsWith("build.gradle.kts"))) {
const normalized = file.replace(/\\/g, "/");
if (!normalized.includes("/app/")) continue;
let content = fs.readFileSync(file, "utf8");
if (content.includes('manifestPlaceholders["usesCleartextTraffic"]')) {
content = content.replace(
/manifestPlaceholders\["usesCleartextTraffic"\]\s*=\s*"[^"]*"/,
'manifestPlaceholders["usesCleartextTraffic"] = "true"',
);
} else if (content.includes("defaultConfig {")) {
content = content.replace(
/defaultConfig\s*\{/,
`defaultConfig {\n manifestPlaceholders["usesCleartextTraffic"] = "true"`,
);
} else {
continue;
}
fs.writeFileSync(file, content);
console.log(`Patched Gradle cleartext: ${path.relative(root, file)}`);
}
}
ensureNetworkSecurityXml(androidRoot);
patchAndroidManifest(androidRoot);
patchGradleCleartext(androidRoot);
console.log("Android app patches applied (system bars + network + camera)");