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 = [ `true`, `?android:colorBackground`, `?android:colorBackground`, ]; for (const item of items) { const name = item.match(/name="([^"]+)"/)?.[1]; if (!name) continue; if (next.includes(`name="${name}"`)) { next = next.replace( new RegExp(`[^<]*`, "g"), item, ); } else { next = next.replace(/<\/style>/, ` ${item}\n `); } } 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 = ` `; 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 = ` \n`; if (xml.includes("/, `${tag}`); } 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( /\n\n ]*)>/, '', ); } if (!xml.includes("networkSecurityConfig")) { xml = xml.replace( /]*)>/, '', ); } 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)");