First slice of Android Phase C (full LiveKit parity), mirroring how iOS started: native full-screen incoming call UI that wakes the app from closed, with call MEDIA still in the WebView (foreground). Native LiveKit media + a foreground service for true background audio are Increment 2. Native (new Android side of the native-call plugin, Java to match the proven official-plugin module layout): - NativeCallPlugin: registered as window.Capacitor.Plugins.NativeCall (same jsName as iOS, so the shared web bridge drives both). getToken (FCM token), reportIncomingCall (ring), reportOutgoingCall, endCall, setMuted; video/screen/hole-punch/transcript methods are safe no-ops until later increments so the shared bridge never rejects. - BzCallMessagingService: extends @capacitor/push-notifications' MessagingService — handles high-priority call-wake DATA messages (data.bzcall==1) to ring when closed, forwards all other push via super. - IncomingCallActivity: full-screen call screen over the lock screen (showWhenLocked/turnScreenOn), Accept/Decline, looping ringtone. - CallStore: incoming-call notification (channel, full-screen-intent, actions) + UUID dedup so the FCM and WebSocket paths don't double-ring. - Plugin manifest: BzCallMessagingService + IncomingCallActivity + call perms (USE_FULL_SCREEN_INTENT/VIBRATE/WAKE_LOCK). - android-patch.js: removes Capacitor's MessagingService in the APP manifest (highest merge priority) so ours is the sole MESSAGING_EVENT receiver; ensures the tools namespace. Idempotent. - package.json: capacitor.android.src wired. Server (dormant until ANDROID_CALLS=1): - push.js sendFcmData: high-priority data-only FCM (rings when closed). sendCallNotification rings 'android' tokens via it; sendCallCancel too. - config.js ANDROID_CALLS flag; /api/meetings/config exposes androidCall. Web (home.html) — platform-aware, iOS/web behavior byte-for-byte unchanged: - setupNativeCall: Android gated on cfg.androidCall, sets _callWebViewMedia (native ring, WebView media); iOS keeps CallKit native-media path. - answerCall + both outgoing sites branch on _callWebViewMedia so Android joins the meeting normally while the callee still rings via the server. Safe to deploy now: dormant until ANDROID_CALLS=1 AND an APK carrying the plugin is installed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
4.1 KiB
JavaScript
71 lines
4.1 KiB
JavaScript
// 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');
|
|
|
|
// (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 <application>. Keyed on bzcCallSvcRemove so re-runs never duplicate it.
|
|
if (!s.includes('bzcCallSvcRemove')) {
|
|
// Ensure xmlns:tools is declared on <manifest> (tools:node="remove" needs it).
|
|
if (!/<manifest[^>]*xmlns:tools=/.test(s)) {
|
|
s = s.replace(/<manifest(\s)/, '<manifest xmlns:tools="http://schemas.android.com/tools"$1');
|
|
}
|
|
const removal = [
|
|
'',
|
|
' <!-- bzcCallSvcRemove: the native-call plugin\'s BzCallMessagingService replaces this one (it forwards regular push via super) -->',
|
|
' <service android:name="com.capacitorjs.plugins.pushnotifications.MessagingService" tools:node="remove" />',
|
|
' ',
|
|
].join('\n');
|
|
const before = s;
|
|
s = s.replace(/<\/application>/, removal + '</application>');
|
|
if (s !== before) console.log(' Capacitor MessagingService removal injected (native-call takes over MESSAGING_EVENT)');
|
|
else console.log(' could not find </application> — 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 = [
|
|
'',
|
|
' <!-- bzcAndroidPerms: permissions the Biz Connect web UI needs (see mobile/resources/android-permissions.xml) -->',
|
|
' <!-- Push notifications: Android 13 (API 33)+ shows a runtime prompt -->',
|
|
' <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />',
|
|
' <!-- Voice / video calls + camera, used by the WebRTC features in the web UI -->',
|
|
' <uses-permission android:name="android.permission.CAMERA" />',
|
|
' <uses-permission android:name="android.permission.RECORD_AUDIO" />',
|
|
' <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />',
|
|
' <!-- Camera is optional hardware (tablets without one can still install) -->',
|
|
' <uses-feature android:name="android.hardware.camera" android:required="false" />',
|
|
'',
|
|
].join('\n');
|
|
const orig = s;
|
|
// Insert directly before the closing </manifest> tag.
|
|
s = s.replace(/<\/manifest>\s*$/, block + '</manifest>\n');
|
|
if (s === orig) { console.log(' could not find </manifest> — 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);
|