Declare CAMERA in AndroidManifest so RustWebChromeClient can show the system permission dialog. Inject Permissions-Policy meta for camera access in the Tauri shell.
228 lines
6.9 KiB
JavaScript
228 lines
6.9 KiB
JavaScript
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)");
|