// Typed wrapper around the CallForegroundService Capacitor plugin. // // The native service (CallForegroundService.java + CallForegroundPlugin.java) // holds an Android foreground service with foregroundServiceType="microphone" // for the duration of an active DM call. Without it, Android 14+ revokes the // mic AppOp and applies background data firewall when the app goes to // background (e.g. screen lock), killing the call. // // import { registerPlugin } from '@capacitor/core'; import { isAndroidPlatform } from '../../utils/capacitor'; export interface IncomingRingUpsert { eventId: string; roomId: string; callerName?: string; // Milliseconds since epoch of the sender's origin timestamp (as reported by // content.sender_ts). Native uses it to compute the expiry alarm so a // late-rendered entry doesn't live past true lifetime. senderTs?: number; // Ring lifetime in milliseconds (content.lifetime, falls back to 30s native-side). lifetime?: number; // Optional — used as google.message_id in the PendingIntent extras so // Capacitor's pushNotificationActionPerformed fires on Answer tap. Left // blank for JS-originated upserts; Java merges richer FCM data on seed. messageId?: string; } interface CallForegroundServicePlugin { start(options?: { title?: string; body?: string }): Promise; stop(): Promise; upsertIncomingRing(options: IncomingRingUpsert): Promise; removeIncomingRing(options: { eventId: string }): Promise; } const plugin = registerPlugin('CallForegroundService'); // Android-only — the native implementation lives in CallForegroundService.java // and CallForegroundPlugin.java. Gating on platform here (not just isNativePlatform) // avoids calling a non-existent plugin path on iOS or any future native target. export const callForegroundService = { start(options?: { title?: string; body?: string }): Promise { if (!isAndroidPlatform()) return Promise.resolve(); return plugin.start(options); }, stop(): Promise { if (!isAndroidPlatform()) return Promise.resolve(); return plugin.stop(); }, // Add/refresh a live incoming ring in the native registry. Called from the // incomingCallsAtom sync effect whenever a key lands in the atom (happy // path). Idempotent with any prior FCM seed for the same eventId — // (atom-sync cleanup + native registry bridge). upsertIncomingRing(entry: IncomingRingUpsert): Promise { if (!isAndroidPlatform()) return Promise.resolve(); return plugin.upsertIncomingRing(entry); }, // Drop a ring from the native registry and tombstone its eventId so a late // FCM/sync re-seed within ring lifetime cannot resurrect it. Called from // atom REMOVE, suppress paths, and the decline-first branch. removeIncomingRing(eventId: string): Promise { if (!isAndroidPlatform()) return Promise.resolve(); return plugin.removeIncomingRing({ eventId }); }, };