fix(android/voip): use phoneCall FGS type and fix inverted accept/decline guard#7233
Conversation
…line guard - VoipCallService: switch foregroundServiceType from `microphone` to `phoneCall` (manifest + ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL). On Android 14+ / targetSdk 36 the microphone type is a "while-in-use" FGS that requires the process to be in a foreground-eligible state at startForeground() time. The accept paths call VoipCallService.startService before MainActivity / the FSI Activity is in that eligible state, producing a SecurityException and a FATAL process crash when the user taps Accept on the call notification (especially from a locked device on Android 16). FOREGROUND_SERVICE_PHONE_CALL is already declared; self-managed Telecom (addNewIncomingCall) covers the phoneCall type. - IncomingCallActivity.handleAccept / handleDecline: invert the compareAndSet guard. `compareAndSet(false, true)` returns true when the CAS succeeds on the first tap; the previous `if (... ) return` therefore returned on the FIRST tap and only allowed subsequent taps through, silently dropping the first Accept/Decline on the locked-screen FSI path.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📜 Recent review details⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
🧰 Additional context used🧠 Learnings (3)📓 Common learnings📚 Learning: 2026-04-22T22:57:58.545ZApplied to files:
📚 Learning: 2026-04-22T21:35:29.021ZApplied to files:
🔇 Additional comments (3)
WalkthroughChanges the VoIP service's foreground service type from microphone to phone call across Android manifest and code configuration. Additionally fixes a concurrency issue in the accept/decline button handlers by inverting the guard logic to prevent duplicate actions on concurrent taps. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Android Build Available Rocket.Chat Experimental 4.72.0.108624 Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNTr-FIEywnCuX-OgZ3TIr69mYdO9M_n_DxvM4TyAN1S2KbsPjUnk8kklP5EWTPE_jPSKpa695hrsUfvx4Ai |
|
Android Build Available Rocket.Chat 4.72.0.108623 Internal App Sharing: https://play.google.com/apps/test/RQQ8k09hlnQ/ahAO29uNTbmU5pKztH6_s2tTv5mYEJbbYrDeEeKZu1givjsf_bpd6sL1Ipe7fdoHMTNVx3giuc2ZIE-LEQTakBx5lS |
…urvives backgrounding Outgoing calls went through `MediaSessionInstance.startCall` directly to the media-signaling SDK and never touched `VoipNotification`, so no foreground service was started. With no FGS in the active set, `RECORD_AUDIO` (while-in-use) is revoked ~5s after the user backgrounds the app, dropping the caller's audio while the peer's still plays. Reported in NATIVE-1178. The incoming-accept path already starts `VoipCallService` from native after Telecom is active. This mirrors that for outgoing by exposing `startVoipCallService(callId)` on the TurboModule and invoking it Android-only from the `newCall` 'caller' branch, while the initiating activity is still visible (so `microphone` FGS-start eligibility is satisfied). FGS type widens from `phoneCall` to `microphone|phoneCall`: - `phoneCall` keeps the incoming-accept entry point startable from IncomingCallActivity / MainActivity.onNewIntent via the active self-managed Telecom call (PR #7233 rationale). - `microphone` is what actually authorises background mic capture — `phoneCall` alone is documented to keep the call alive via ConnectionService but does not exempt `RECORD_AUDIO` from the while-in-use clock. Both bits are safe to start in either path because both paths reach `startForeground()` from a while-in-use eligible state: outgoing from a visible activity, incoming after `connection.onAnswer()` puts Telecom into STATE_ACTIVE.
Proposed changes
Two independent bugs surfaced in user reports on Android 16 (targetSdk=36) when a VoIP call arrives on a locked device:
Bug 1 —
VoipCallServicemicrophone FGS eligibility violation (process crash).VoipCallService.startForegroundWithNotificationwas callingstartForeground(..., FOREGROUND_SERVICE_TYPE_MICROPHONE)unconditionally on API 29+. On Android 14+ the microphone type is a "while-in-use" FGS and requires the process to be in a foreground-eligible state atstartForeground()time. Our accept paths callVoipCallService.startServiceas the first statement ofVoipNotification.handleAcceptAction, which runs from eitherIncomingCallActivity(FSI, above-lock separate-task Activity) orMainActivity.onNewIntent(HUN, not yet resumed) — neither context satisfies the eligibility check on targetSdk 36. The result is aFATAL EXCEPTION: maincaused bySecurityException: Starting FGS with type microphone ... requires permissions [FOREGROUND_SERVICE_MICROPHONE] and any of [RECORD_AUDIO, ...] and the app must be in the eligible state/exemptions.Switching to
FOREGROUND_SERVICE_TYPE_PHONE_CALLresolves this: thephoneCalltype's eligibility is satisfied by the self-managed Telecom call already registered viatelecomManager.addNewIncomingCallinVoipNotification.registerCallWithTelecomManager.FOREGROUND_SERVICE_PHONE_CALLis already declared in the manifest.Bug 2 — Inverted
acceptDeclineGuardinIncomingCallActivity(first-tap no-op on locked screen).handleAccept/handleDeclineboth hadif (acceptDeclineGuard.compareAndSet(false, true)) return.AtomicBoolean.compareAndSet(false, true)returnstrueon the first call (CAS succeeds), so the guard was returning on the FIRST tap and only letting subsequent taps through — the opposite of a single-fire guard. This is only observable on the FSI/locked-screen path (the HUN path goes throughMainActivity), which matches the "accepting while locked doesn't open the app" symptom.Inverting to
if (!acceptDeclineGuard.compareAndSet(false, true)) returnrestores the intended "first tap proceeds, subsequent taps dropped" semantics.Issue(s)
User-reported FATAL crash on Android 16 tapping incoming VoIP call notification; lock-screen Accept appearing to do nothing.
How to test or reproduce
VoipCallService, so Bug 1's fix applies here as well.Screenshots
N/A — native Android behavior change.
Types of changes
Checklist
Further comments
CallKeep's own
VoiceConnectionServicealready declaresmicrophone|phoneCallin the manifest but is never actually promoted to FGS at runtime becauseRNCallKeep.setup()is not passed aforegroundServiceconfig from the JS side — soVoipCallServiceis the only FGS keeping the audio session alive. A cleaner future refactor would pass that config to CallKeep and removeVoipCallServiceentirely, relying on the Telecom-exempted FGS path. That is out of scope for this surgical fix.Known separate observation (not addressed here): the
registerCallWithTelecomManagercatches atVoipNotification.kt:794-798silently swallowSecurityException/ExceptionatLog.eseverity only. IfaddNewIncomingCallfails, the Telecom FGS exemption is never granted andphoneCalleligibility may still fail. Worth tracking in a separate issue.Summary by CodeRabbit