// Inject the runtime permissions the web UI needs into the Capacitor-generated AndroidManifest.xml. // Run on Codemagic from the Android workflow: // node mobile/scripts/android-patch.js mobile/android/app/src/main/AndroidManifest.xml // // WHY: mobile/android/ is gitignored (regenerated in CI by `cap add android`, same as iOS ios/). The // freshly generated manifest only declares INTERNET, so without this the WebRTC calls in the web UI can't // get camera/mic and Android 13+ never prompts for notifications. This adds the same permissions listed in // mobile/resources/android-permissions.xml. (When the native-call Android plugin lands, it will contribute // its own manifest entries via Capacitor manifest-merging; this only covers the app-level WebView perms.) // // TOLERANT: exits 0 and no-ops if anything is off, so it can NEVER fail the build. Idempotent (keyed on // the bzcAndroidPerms marker), so re-runs never duplicate the block. const fs = require('fs'); const p = process.argv[2]; if (!p || !fs.existsSync(p)) { console.log(' (AndroidManifest.xml not found — permission patch skipped)'); process.exit(0); } try { let s = fs.readFileSync(p, 'utf8'); // (0) LiveKit Android (native call media, Increment 2) requires a higher minSdk than Capacitor's default. // Bump it in the generated variables.gradle so the library manifest merge holds (a library minSdk above the // app's fails the build). variables.gradle sits at android/variables.gradle (3 dirs up from app/src/main). try { const path = require('path'); const varsPath = path.resolve(path.dirname(p), '..', '..', '..', 'variables.gradle'); if (fs.existsSync(varsPath)) { let v = fs.readFileSync(varsPath, 'utf8'); const m = v.match(/minSdkVersion\s*=\s*(\d+)/); if (m && parseInt(m[1], 10) < 24) { v = v.replace(/minSdkVersion\s*=\s*\d+/, 'minSdkVersion = 24'); fs.writeFileSync(varsPath, v); console.log(' minSdkVersion bumped to 24 (LiveKit Android floor)'); } else { console.log(' minSdkVersion already >= 24 or not found — no bump'); } } else { console.log(' variables.gradle not found — minSdk bump skipped'); } } catch (e) { console.log(' minSdk bump skipped:', e.message); } // (0b) JitPack repository for LiveKit Android's transitive deps (Increment 2). LiveKit 2.29.2 pulls in // com.github.davidliu:audioswitch — a JitPack-only fork pinned to a commit-hash version. Gradle resolves // the FULL transitive graph of :app:debugRuntimeClasspath against the APP module's (+ allprojects) // repositories, NOT the native-call plugin's, so a jitpack repo declared only in the plugin is ignored and // the build dies with "Could not find com.github.davidliu:audioswitch". Inject jitpack where :app sees it: // the generated root build.gradle's allprojects{} block (Capacitor's template) and, if a newer template // uses it instead, settings.gradle's dependencyResolutionManagement{}. Idempotent (keyed on jitpack.io), // tolerant (any miss just logs + continues — never fails the build). try { const path = require('path'); const base = path.resolve(path.dirname(p), '..', '..', '..'); // android/ const jitLine = " maven { url 'https://jitpack.io' } // bzcJitpack: LiveKit Android transitive deps"; // root build.gradle → allprojects { repositories { ... } } (classic Capacitor template) const rootGradle = path.join(base, 'build.gradle'); if (fs.existsSync(rootGradle)) { let g = fs.readFileSync(rootGradle, 'utf8'); if (g.includes('jitpack.io')) { console.log(' jitpack already present in build.gradle'); } else { const re = /(allprojects\s*\{\s*repositories\s*\{)/; if (re.test(g)) { g = g.replace(re, '$1\n' + jitLine); fs.writeFileSync(rootGradle, g); console.log(' jitpack repo injected into allprojects (root build.gradle)'); } else { console.log(' allprojects{repositories{}} not found in build.gradle — jitpack root inject skipped'); } } } else { console.log(' root build.gradle not found — jitpack root inject skipped'); } // settings.gradle → dependencyResolutionManagement { repositories { ... } } (newer templates, if any) const settingsGradle = path.join(base, 'settings.gradle'); if (fs.existsSync(settingsGradle)) { let s2 = fs.readFileSync(settingsGradle, 'utf8'); if (s2.includes('jitpack.io')) { console.log(' jitpack already present in settings.gradle'); } else { const re2 = /(dependencyResolutionManagement\s*\{[\s\S]*?repositories\s*\{)/; if (re2.test(s2)) { s2 = s2.replace(re2, '$1\n' + jitLine); fs.writeFileSync(settingsGradle, s2); console.log(' jitpack repo injected into dependencyResolutionManagement (settings.gradle)'); } else { console.log(' no dependencyResolutionManagement in settings.gradle — nothing to patch there'); } } } } catch (e) { console.log(' jitpack inject skipped:', e.message); } // (1) Native-call FirebaseMessagingService swap. The native-call plugin declares its own MESSAGING_EVENT // service (BzCallMessagingService) that handles call-wake data messages AND forwards regular push to Capacitor // via super. Remove @capacitor/push-notifications' own MessagingService here — the APP manifest is the highest // merge priority, so this reliably guarantees exactly one receiver. Needs the `tools` namespace + an insert // inside . Keyed on bzcCallSvcRemove so re-runs never duplicate it. if (!s.includes('bzcCallSvcRemove')) { // Ensure xmlns:tools is declared on (tools:node="remove" needs it). if (!/]*xmlns:tools=/.test(s)) { s = s.replace(/', ' ', ' ', ].join('\n'); const before = s; s = s.replace(/<\/application>/, removal + ''); if (s !== before) console.log(' Capacitor MessagingService removal injected (native-call takes over MESSAGING_EVENT)'); else console.log(' could not find — MessagingService removal skipped'); fs.writeFileSync(p, s); } else { console.log(' Capacitor MessagingService removal already present'); } // (2) WebView permissions. if (s.includes('bzcAndroidPerms') || s.includes('android.permission.RECORD_AUDIO')) { console.log(' Android permissions already present'); process.exit(0); } const block = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); const orig = s; // Insert directly before the closing tag. s = s.replace(/<\/manifest>\s*$/, block + '\n'); if (s === orig) { console.log(' could not find — permission patch skipped'); process.exit(0); } fs.writeFileSync(p, s); console.log(' Android permissions injected into ' + p); } catch (e) { console.log(' Android permission patch skipped:', e.message); } process.exit(0);