/** * FNXC:TaskDetailAndroidBack 2026-07-05-11:40: * FN-7583: the Android system back GESTURE (predictive back / edge swipe, Android 13+, * default-prominent at this project's `targetSdk 35`) was not reaching the same * `@capacitor/app` `OnBackPressedCallback` ("backButton" JS event) that the hardware/ * legacy Back button already routes through — even though both the button and the * gesture are supposed to funnel through AndroidX's `OnBackPressedDispatcher`. Per * Android's predictive-back docs (https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture) * and the confirmed upstream defect (ionic-team/capacitor-plugins#2418, mitigated by * ionic-team/capacitor-plugins#2390's `disableBackButtonHandler` toggle shipped in * `@capacitor/app@7.1.0`), an app must opt in via `android:enableOnBackInvokedCallback="true"` * on the manifest `` tag for the gesture-completion callback to reliably * reach a registered `OnBackPressedCallback`. Without that opt-in, the OS falls back to * "legacy" gesture dispatch, which silently drops the callback invocation for the * gesture while the hardware Back button (delivered via the ordinary Window/Activity * input path) keeps working — exactly the divergence this task reported. * * The native `android/` project is generated by `cap sync` and is git-ignored (see * AGENTS.md / `.gitignore`), so durable changes cannot live there directly. Instead this * script idempotently patches the generated manifest immediately after every sync, and * is wired into Capacitor's own `capacitor:sync:after` npm-script hook (see * `package.json`), which the Capacitor CLI runs automatically after `cap sync` — and * therefore after `cap run android` (which syncs first) and `build:mobile` too. * * This keeps a SINGLE dismissal invariant: gesture and button both end up delivered to * `AndroidBackButtonManager`'s `backButton` listener in * `packages/mobile/src/plugins/native-shell.ts`, which dispatches the cancelable * `fusion:native-back` event consumed by the dashboard's shared nav-history stack. */ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const ENABLE_ATTR = 'android:enableOnBackInvokedCallback="true"'; export function patchManifestSource(xml: string): { changed: boolean; xml: string } { const applicationTagMatch = xml.match(/]*>/); if (!applicationTagMatch) { return { changed: false, xml }; } const applicationTag = applicationTagMatch[0]; if (applicationTag.includes("android:enableOnBackInvokedCallback")) { // Already opted in (idempotent — do not duplicate on repeat `cap sync` runs). return { changed: false, xml }; } const patchedTag = applicationTag.replace(/>$/, ` ${ENABLE_ATTR}>`); const patchedXml = xml.slice(0, applicationTagMatch.index) + patchedTag + xml.slice((applicationTagMatch.index ?? 0) + applicationTag.length); return { changed: true, xml: patchedXml }; } export function resolveManifestPath(mobilePackageDir: string): string { return join(mobilePackageDir, "android", "app", "src", "main", "AndroidManifest.xml"); } export function patchAndroidManifest(mobilePackageDir: string): { patched: boolean; skipped: boolean; path: string } { const manifestPath = resolveManifestPath(mobilePackageDir); if (!existsSync(manifestPath)) { // No Android platform added yet (e.g. iOS-only sync) — safe no-op, not an error. return { patched: false, skipped: true, path: manifestPath }; } const original = readFileSync(manifestPath, "utf8"); const { changed, xml } = patchManifestSource(original); if (changed) { writeFileSync(manifestPath, xml, "utf8"); } return { patched: changed, skipped: false, path: manifestPath }; } function isMainModule(): boolean { return process.argv[1] === fileURLToPath(import.meta.url); } if (isMainModule()) { const mobilePackageDir = dirname(fileURLToPath(import.meta.url)).replace(/[/\\]scripts$/, ""); const result = patchAndroidManifest(mobilePackageDir); if (result.skipped) { console.log(`[patch-android-manifest] no android/ project at ${result.path}; skipping (nothing to patch yet)`); } else if (result.patched) { console.log(`[patch-android-manifest] added android:enableOnBackInvokedCallback="true" to ${result.path}`); } else { console.log(`[patch-android-manifest] ${result.path} already opts into enableOnBackInvokedCallback; no change`); } }