Skip to content

fix(android): handle null VoiceConnection in answerIncomingCall, notify JS#7201

Merged
diegolmello merged 3 commits into
feat.voip-lib-newfrom
fix/voip-null-guard
Apr 22, 2026
Merged

fix(android): handle null VoiceConnection in answerIncomingCall, notify JS#7201
diegolmello merged 3 commits into
feat.voip-lib-newfrom
fix/voip-null-guard

Conversation

@diegolmello

@diegolmello diegolmello commented Apr 22, 2026

Copy link
Copy Markdown
Member

Proposed changes

Android null guard: answerIncomingCall now handles the null VoiceConnection case (Telecom connection killed by the system). It cleans up Telecom (disconnectIncomingCall, cancelById), stops DDP (ddpRegistry.stopClient), and stashes a voipAcceptFailed payload via VoipModule.storeAcceptFailureForJs so JS shows a VoIP_Call_Issue toast instead of leaving the UI hanging.

TS log cleanup: Removed the // TODO: Show error message? comment from the "Call not found after accept" branch in MediaSessionInstance.answerCall. The toast is already wired through the voipAcceptFailed deep-link saga, so the TODO was stale.

Issue(s)

N/A

How to test or reproduce

  1. Start an incoming VoIP call on Android.
  2. Force a scenario where Telecom's VoiceConnection is gone by the time answerIncomingCall runs (e.g. system kills the connection under memory pressure, or the connection is canceled before accept completes).
  3. Verify the user sees the VoIP_Call_Issue toast instead of a hanging call UI, and that DDP/Telecom state is cleaned up.

Screenshots

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Further comments

Files changed

  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt
    • answerIncomingCall now takes a Context parameter.
    • Null-VoiceConnection branch: calls disconnectIncomingCall, cancelById, ddpRegistry.stopClient, and VoipModule.storeAcceptFailureForJs with voipAcceptFailed = true to notify JS.
  • app/lib/services/voip/MediaSessionInstance.ts
    • Cleaned up the "Call not found after accept" log; removed stale TODO comment since the toast path is already wired through the deep-link saga.

Acceptance criteria

Criterion Status
Android null return triggers Telecom + DDP cleanup disconnectIncomingCall, cancelById, ddpRegistry.stopClient
Android null return notifies JS VoipModule.storeAcceptFailureForJs with voipAcceptFailed = true
User sees error message (toast) storeAcceptFailureForJs -> VoipAcceptFailed event -> deep-link saga -> VoIP_Call_Issue toast

Summary by CodeRabbit

  • Bug Fixes

    • Improved VoIP call acceptance error handling with recovery steps when calls fail to connect, including notification cancellation and call cleanup.
    • Enhanced error logging to surface warnings for failed accept flows.
  • Tests

    • Updated integration test expectations to match revised VoIP warning messaging after accept failures.

@diegolmello

Copy link
Copy Markdown
Member Author

Code Review — PR #7201

Files Reviewed: VoipNotification.kt, VoipCallService.kt, AndroidManifest.xml, MediaSessionInstance.ts
Issues: 0 CRITICAL/HIGH, 2 MEDIUM observations


[MEDIUM] answerIncomingCall signature changed — all callers need Context

File: VoipNotification.kt:305

answerIncomingCall now takes a Context parameter. The call site at line 253 passes appCtx (from the outer handleAcceptAction scope), which is correct. However, verify there are no other call sites in the codebase that call answerIncomingCall without the Context parameter.

Checked: grep search confirms only one call site in handleAcceptAction. ✅


[MEDIUM] Null VoiceConnection error payload is minimal

File: VoipNotification.kt:318-328

When answerIncomingCall encounters a null VoiceConnection, it creates a minimal VoipPayload with empty strings for caller, username, host, type, hostName and null for avatarUrl/createdAt. This is intentional (we don't have the original payload in the null case), but the deep-linking saga's handleVoipAcceptFailed checks params.username for navigation and params.voipAcceptFailed for the toast.

No action required — voipAcceptFailed: true ensures the toast fires even with empty navigation params.


Acceptance Criteria Verification

Criterion Status Notes
Android null return triggers state cleanup disconnectIncomingCall, ddpRegistry.stopClient, storeAcceptFailureForJs
iOS null return triggers RNCallKeep.endCall + cleanup Already in handleNativeAccept finishAccept(false) path
User sees error message (toast) storeAcceptFailureForJsVoipAcceptFailed event → VoIP_Call_Issue toast
Both platforms behave consistently Both paths stash failure for JS recovery

Verdict

LGTM — All 4 acceptance criteria satisfied. Both MEDIUM observations are informational only.

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Android: pass application context into the VoIP accept flow and add recovery steps when a voice connection is missing. TypeScript and tests: update warning text to indicate the call was not found after accept.

Changes

Cohort / File(s) Summary
Android VoIP accept flow
android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt
Change answerIncomingCall signature to accept Context; handleAcceptAction now calls answerIncomingCall(appCtx, payload.callId); when VoiceConnectionService.getConnection(callId) returns null, log a warning and perform recovery: disconnectIncomingCall(callId, false), cancelById(appCtx, 0), ddpRegistry.stopClient(callId), and persist an accept-failure VoipPayload for JS via VoipModule.storeAcceptFailureForJs(...).
TypeScript warning message
app/lib/services/voip/MediaSessionInstance.ts
Update console warning text from "[VoIP] Call not found:" to "[VoIP] Call not found after accept:".
Integration test update
app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsx
Adjust console warning allowlist and assertion to expect "[VoIP] Call not found after accept:".

Sequence Diagram(s)

sequenceDiagram
  participant Notification as VoipNotification
  participant AndroidSvc as VoiceConnectionService
  participant DDP as DdpRegistry
  participant VoipModule as VoipModule(JS)

  Notification->>Notification: handleAcceptAction(appCtx, payload)
  Notification->>Notification: answerIncomingCall(appCtx, callId)
  Notification->>AndroidSvc: getConnection(callId)
  alt connection found
    AndroidSvc-->>Notification: connection
    Notification->>AndroidSvc: onAnswer / proceed with connection
  else connection null
    AndroidSvc-->>Notification: null
    Notification->>Notification: log warning
    Notification->>Notification: disconnectIncomingCall(callId, false)
    Notification->>Notification: cancelById(appCtx, 0)
    Notification->>DDP: stopClient(callId)
    Notification->>VoipModule: storeAcceptFailureForJs(callId, payload)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding null-guard handling for VoiceConnection in answerIncomingCall with JS notification on Android.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt (1)

248-329: ⚠️ Potential issue | 🟠 Major

Don’t let the null-connection path fall through as a successful accept.

Line 318 stores an accept failure, but answerIncomingCall() returns Unit, so Line 254 still stores the normal initial call event after native cleanup. That can make JS process both “accept failed” and “incoming call” state. Return a success/failure result from native answer and only call storeInitialEvents when Telecom actually had a connection.

Proposed control-flow fix
                 ddpRegistry.stopClient(payload.callId)
                 if (answerRequestSucceeded) {
-                    answerIncomingCall(appCtx, payload.callId)
-                    VoipModule.storeInitialEvents(payload)
+                    if (answerIncomingCall(appCtx, payload)) {
+                        VoipModule.storeInitialEvents(payload)
+                    }
                 } else {
                     Log.d(TAG, "media-calls.answer failed for ${payload.callId}; opening app for JS recovery")
                     disconnectIncomingCall(payload.callId, false)
                     VoipModule.storeAcceptFailureForJs(payload)
                 }
@@
-        private fun answerIncomingCall(context: Context, callId: String) {
+        private fun answerIncomingCall(context: Context, payload: VoipPayload): Boolean {
+            val callId = payload.callId
             val connection = VoiceConnectionService.getConnection(callId)
-            when (connection) {
-                is VoiceConnection -> connection.onAnswer()
+            return when (connection) {
+                is VoiceConnection -> {
+                    connection.onAnswer()
+                    true
+                }
                 null -> {
                     // C3 fix: null return means Telecom connection is gone (system killed it).
                     // Notify JS so the user sees an error instead of a hanging UI.
                     Log.w(TAG, "No active VoiceConnection for accepted call: $callId — notifying JS of failure")
                     val appCtx = context.applicationContext
                     disconnectIncomingCall(callId, false)
-                    cancelById(appCtx, 0)
+                    cancelById(appCtx, payload.notificationId)
                     ddpRegistry.stopClient(callId)
                     // Stash failure payload for JS: it will show error toast on getInitialEvents.
                     VoipModule.storeAcceptFailureForJs(VoipPayload(
-                        callId = callId,
-                        caller = "",
-                        username = "",
-                        host = "",
-                        type = "",
-                        hostName = "",
-                        avatarUrl = null,
-                        createdAt = null,
+                        callId = payload.callId,
+                        caller = payload.caller,
+                        username = payload.username,
+                        host = payload.host,
+                        type = payload.type,
+                        hostName = payload.hostName,
+                        avatarUrl = payload.avatarUrl,
+                        createdAt = payload.createdAt,
                         voipAcceptFailed = true
                     ))
+                    false
                 }
-                else -> Log.d(TAG, "Non-VoiceConnection for accept, callId: $callId")
+                else -> {
+                    Log.d(TAG, "Non-VoiceConnection for accept, callId: $callId")
+                    false
+                }
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt`
around lines 248 - 329, The null-connection path in answerIncomingCall currently
only logs and stores a failure payload but returns Unit, so finish(...) still
treats the accept as succeeded and calls VoipModule.storeInitialEvents; change
answerIncomingCall(context: Context, callId: String) to return Boolean (true
when a VoiceConnection was found and .onAnswer() called, false when null path
taken), update finish(answerRequestSucceeded: Boolean) to call
answerIncomingCall(...) and combine its Boolean result with
answerRequestSucceeded before calling VoipModule.storeInitialEvents (i.e., only
call storeInitialEvents when both the network answer succeeded and
answerIncomingCall returned true), and keep the existing null-path cleanup
(disconnectIncomingCall, cancelById, ddpRegistry.stopClient,
VoipModule.storeAcceptFailureForJs) inside the now-returning-false branch;
locate these changes around the finish function and the answerIncomingCall and
VoiceConnectionService.getConnection usages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@android/app/src/main/java/chat/rocket/reactnative/voip/VoipCallService.kt`:
- Around line 70-79: The service currently returns START_STICKY for ACTION_START
which can cause Android to recreate the service with a null intent; update
VoipCallService (onStartCommand) so the ACTION_START branch returns
START_NOT_STICKY instead of START_STICKY, and modify the unknown/else branch to
call stopSelf(startId) to ensure the service is terminated when intent is
invalid (retain logging and ensure startForegroundWithNotification(callId) is
still called for ACTION_START). Use the ACTION_START symbol, the
startForegroundWithNotification(callId) call, and the onStartCommand startId
parameter to locate where to change the return value and insert
stopSelf(startId).
- Around line 48-54: The stopService static helper currently calls
context.startService(intent) which can throw on Android O+ when called from
background; change VoipCallService.stopService(context) to call
context.stopService(intent) instead so the Intent (with ACTION_STOP) is sent via
Context.stopService to stop the foreground service safely from backgrounded
contexts. Locate the stopService method in VoipCallService and replace the
context.startService call with context.stopService using the same Intent.

---

Outside diff comments:
In `@android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt`:
- Around line 248-329: The null-connection path in answerIncomingCall currently
only logs and stores a failure payload but returns Unit, so finish(...) still
treats the accept as succeeded and calls VoipModule.storeInitialEvents; change
answerIncomingCall(context: Context, callId: String) to return Boolean (true
when a VoiceConnection was found and .onAnswer() called, false when null path
taken), update finish(answerRequestSucceeded: Boolean) to call
answerIncomingCall(...) and combine its Boolean result with
answerRequestSucceeded before calling VoipModule.storeInitialEvents (i.e., only
call storeInitialEvents when both the network answer succeeded and
answerIncomingCall returned true), and keep the existing null-path cleanup
(disconnectIncomingCall, cancelById, ddpRegistry.stopClient,
VoipModule.storeAcceptFailureForJs) inside the now-returning-false branch;
locate these changes around the finish function and the answerIncomingCall and
VoiceConnectionService.getConnection usages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f3ed8bc0-137b-40ac-8e39-179b460436c7

📥 Commits

Reviewing files that changed from the base of the PR and between d30c291 and 3d9a839.

📒 Files selected for processing (4)
  • android/app/src/main/AndroidManifest.xml
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipCallService.kt
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt
  • app/lib/services/voip/MediaSessionInstance.ts
📜 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)
  • GitHub Check: ESLint and Test / run-eslint-and-test
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,jsx,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Configure Prettier with tabs, single quotes, 130 character width, no trailing commas, arrow parens avoid, and bracket same line

Files:

  • app/lib/services/voip/MediaSessionInstance.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ESLint with @rocket.chat/eslint-config base configuration including React, React Native, TypeScript, and Jest plugins

Files:

  • app/lib/services/voip/MediaSessionInstance.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use TypeScript with strict mode enabled and configure baseUrl to app/ for import resolution

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

Files:

  • app/lib/services/voip/MediaSessionInstance.ts
app/lib/services/voip/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Implement VoIP with WebRTC peer-to-peer audio calls in app/lib/services/voip/ using Zustand stores instead of Redux, with native CallKit (iOS) and Telecom (Android) integration; keep VoIP and VideoConf separate

Files:

  • app/lib/services/voip/MediaSessionInstance.ts
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/lib/services/voip/MediaSessionInstance.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/lib/services/voip/**/*.{ts,tsx} : Implement VoIP with WebRTC peer-to-peer audio calls in app/lib/services/voip/ using Zustand stores instead of Redux, with native CallKit (iOS) and Telecom (Android) integration; keep VoIP and VideoConf separate
📚 Learning: 2026-04-07T17:49:17.538Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/lib/services/voip/**/*.{ts,tsx} : Implement VoIP with WebRTC peer-to-peer audio calls in app/lib/services/voip/ using Zustand stores instead of Redux, with native CallKit (iOS) and Telecom (Android) integration; keep VoIP and VideoConf separate

Applied to files:

  • app/lib/services/voip/MediaSessionInstance.ts
  • android/app/src/main/AndroidManifest.xml
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipCallService.kt
🔇 Additional comments (2)
android/app/src/main/AndroidManifest.xml (1)

150-155: LGTM — service is scoped correctly.

The service is non-exported and declares the microphone foreground service type used by VoipCallService.

app/lib/services/voip/MediaSessionInstance.ts (1)

167-167: LGTM — clearer native-accept fallback log.

The updated warning better identifies the post-accept missing-call path without changing behavior.

@diegolmello diegolmello changed the title fix(both): guard null returns in answerIncomingCall/handleNativeAccept with RNCallKeep.endCall + error fix(voip): guard null returns in answerIncomingCall/handleNativeAccept with RNCallKeep.endCall + error Apr 22, 2026
@diegolmello diegolmello force-pushed the fix/voip-null-guard branch from 3d9a839 to 39c8113 Compare April 22, 2026 15:08
…t with RNCallKeep.endCall + error

**C3 fix (Android):** `answerIncomingCall` now takes `Context` and handles the null
`VoiceConnection` case by cleaning up the Telecom connection, stopping DDP, and
stashing a `voipAcceptFailed` payload so JS shows a toast error.

**TS cleanup:** Removed `// TODO: Show error message?` comment from `answerCall`.
The `voipAcceptFailed` path is handled by the existing deep-linking saga which
already shows `VoIP_Call_Issue` toast.

Fixes: C3 (null returns silently fail)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt (1)

256-267: ⚠️ Potential issue | 🟠 Major

Short-circuit the accept flow when VoiceConnection is gone.

Line 326 stashes the accept-failed event, but answerIncomingCall returns Unit, so Line 262 still stores the normal initial payload afterward. Also, the foreground service started before the REST accept is not stopped on this null/failure path.

Proposed fix
                 ddpRegistry.stopClient(payload.callId)
                 if (answerRequestSucceeded) {
-                    answerIncomingCall(appCtx, payload.callId)
-                    VoipModule.storeInitialEvents(payload)
+                    if (answerIncomingCall(appCtx, payload.callId)) {
+                        VoipModule.storeInitialEvents(payload)
+                    } else {
+                        VoipCallService.stopService(appCtx)
+                    }
                 } else {
                     Log.d(TAG, "media-calls.answer failed for ${payload.callId}; opening app for JS recovery")
                     disconnectIncomingCall(payload.callId, false)
+                    VoipCallService.stopService(appCtx)
                     VoipModule.storeAcceptFailureForJs(payload)
                 }
-        private fun answerIncomingCall(context: Context, callId: String) {
+        private fun answerIncomingCall(context: Context, callId: String): Boolean {
             val connection = VoiceConnectionService.getConnection(callId)
-            when (connection) {
-                is VoiceConnection -> connection.onAnswer()
+            return when (connection) {
+                is VoiceConnection -> {
+                    connection.onAnswer()
+                    true
+                }
                 null -> {
                     // C3 fix: null return means Telecom connection is gone (system killed it).
                     // Notify JS so the user sees an error instead of a hanging UI.
                     Log.w(TAG, "No active VoiceConnection for accepted call: $callId — notifying JS of failure")
@@
                         createdAt = null,
                         voipAcceptFailed = true
                     ))
+                    false
                 }
-                else -> Log.d(TAG, "Non-VoiceConnection for accept, callId: $callId")
+                else -> {
+                    Log.d(TAG, "Non-VoiceConnection for accept, callId: $callId")
+                    false
+                }
             }
         }

Also applies to: 313-337

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt`
around lines 256 - 267, In finish(answerRequestSucceeded: Boolean) short-circuit
the "accept" success path when the underlying VoiceConnection is missing or
answerIncomingCall failed: after calling answerIncomingCall(appCtx,
payload.callId) check that a valid VoiceConnection was returned (or that
answerIncomingCall indicates success) and if it's null/failed, stop the
foreground service started earlier, call disconnectIncomingCall(payload.callId,
false), call VoipModule.storeAcceptFailureForJs(payload) and return early so
VoipModule.storeInitialEvents(payload) is not executed; apply the same guard in
the equivalent block around the other accept path (lines ~313-337) to keep
behavior consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt`:
- Around line 256-267: In finish(answerRequestSucceeded: Boolean) short-circuit
the "accept" success path when the underlying VoiceConnection is missing or
answerIncomingCall failed: after calling answerIncomingCall(appCtx,
payload.callId) check that a valid VoiceConnection was returned (or that
answerIncomingCall indicates success) and if it's null/failed, stop the
foreground service started earlier, call disconnectIncomingCall(payload.callId,
false), call VoipModule.storeAcceptFailureForJs(payload) and return early so
VoipModule.storeInitialEvents(payload) is not executed; apply the same guard in
the equivalent block around the other accept path (lines ~313-337) to keep
behavior consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f606d151-75f7-4f49-b671-068dab769fb3

📥 Commits

Reviewing files that changed from the base of the PR and between 3d9a839 and d8116f7.

📒 Files selected for processing (2)
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt
  • app/lib/services/voip/MediaSessionInstance.ts
✅ Files skipped from review due to trivial changes (1)
  • app/lib/services/voip/MediaSessionInstance.ts
📜 Review details
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/lib/services/voip/**/*.{ts,tsx} : Implement VoIP with WebRTC peer-to-peer audio calls in app/lib/services/voip/ using Zustand stores instead of Redux, with native CallKit (iOS) and Telecom (Android) integration; keep VoIP and VideoConf separate
📚 Learning: 2026-04-07T17:49:17.538Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/lib/services/voip/**/*.{ts,tsx} : Implement VoIP with WebRTC peer-to-peer audio calls in app/lib/services/voip/ using Zustand stores instead of Redux, with native CallKit (iOS) and Telecom (Android) integration; keep VoIP and VideoConf separate

Applied to files:

  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt

@diegolmello diegolmello changed the title fix(voip): guard null returns in answerIncomingCall/handleNativeAccept with RNCallKeep.endCall + error fix(android): handle null VoiceConnection in answerIncomingCall, notify JS Apr 22, 2026
The log message in MediaSessionInstance was updated to include "after accept"
for disambiguation, so the integration test's expectation + console allowlist
entry need to match. Also updates the related comment block.
Rewords the "C3 fix: ..." preface to a self-contained explanation of the
null-VoiceConnection branch without embedding the plan identifier.
@diegolmello diegolmello merged commit 7301db6 into feat.voip-lib-new Apr 22, 2026
4 of 6 checks passed
@diegolmello diegolmello deleted the fix/voip-null-guard branch April 22, 2026 16:25
@diegolmello diegolmello had a problem deploying to experimental_android_build April 22, 2026 16:25 — with GitHub Actions Failure
@diegolmello diegolmello had a problem deploying to official_android_build April 22, 2026 16:25 — with GitHub Actions Failure
@diegolmello diegolmello had a problem deploying to experimental_ios_build April 22, 2026 16:25 — with GitHub Actions Failure
diegolmello added a commit that referenced this pull request Apr 22, 2026
…/Decline (#7215)

* merge feat.voip-lib

* feat(voip): enhance call handling with UUID mapping and event listeners

* Base call UI

* feat(voip): integrate Zustand for call state management and enhance CallView UI

* feat(voip): add simulateCall function for mock call handling in UI development

* refactor(CallView): update button handlers and improve UI responsiveness

* Add pause-shape-unfilled icon

* Base CallHeader

* toggleFocus

* collapse buttons

* Header components

* Hide header when no call

* Timer

* Add use memo

* Add voice call item on sidebar

* cleanup

* Temp use @rocket.chat/media-signaling from .tgz

* cleanup

* Check module and permissions to enable voip

* Refactor stop method to use optional chaining for media signal listeners

* voip push first test

* Add VoIP call handling with pending call management

- Implemented VoIP push notification handling in index.js, including storing call info for later processing.
- Added CallKeep event handlers for answering and ending calls from a cold start.
- Introduced a new CallIdUUID module to convert call IDs to deterministic UUIDs for compatibility with CallKit.
- Created a pending call store to manage incoming calls when the app is not fully initialized.
- Updated deep linking actions to include VoIP call handling.
- Enhanced MediaSessionInstance to process pending calls and manage call states effectively.

* Remove pending store and create getInitialEvents on app/index

* Attempt to make iOS calls work from cold state

* lint and format

* Patch callkeep ios

* Temp send iOS voip push token on gcm

* Temp fix require cycle

* chore: format code and fix lint issues [skip ci]

* CallIDUUID module on android and voip push

* Add setCallUUID on useCallStore to persist calls accepted on native Android

* remove callkeep from notification

* Android Incoming Call UI POC

* Refactor VoIP handling: Migrate VoIP-related classes to a new package structure, removing deprecated modules and consolidating functionality. Update imports in MainApplication and NotificationIntentHandler to reflect changes. This cleanup enhances code organization and prepares for future VoIP feature enhancements.

* Remove VoipForegroundService

* cleanup and use caller instead of callerName

* Cleanup and make iOS build again

* Refactor VoIP handling: Remove unused event emissions for call answered and declined, switch from SharedPreferences to in-memory storage for pending VoIP call data, and update method signatures for better clarity. This cleanup enhances performance and prepares for future VoIP feature improvements.

* Refactor VoIP handling: Introduce a new VoipPayload class to encapsulate call data, streamline notification processing, and enhance method signatures across the VoIP module. This update improves code clarity and prepares for future feature enhancements.

* Migrate react-native-voip-push-notifications to VoipModule

* Refactor VoIP module: Update package structure by moving VoipTurboPackage to the main package and removing the obsolete NativeVoipSpec class. Adjust imports in MainApplication and VoipModule to reflect these changes, enhancing code organization and maintainability.

* Unify emitters

* Move CallKeep listeners from MediaSessionInstance to getInitialEvents

* Clear callkeep on endcall

* Unify getInitialEvents logic

* getInitialEvents -> MediaCallEvents

* chore: format code and fix lint issues [skip ci]

* feat(Android): Add full screen incoming call (#6977)

* feat: Update call UI (#6990)

* feat: Handle audio routing, e.g., Bluetooth headset vs. internal speaker switching (#6992)

* fix: empty space when not on call (#6993)

* feat: Dialpad (#7000)

* action: organized translations

* feat: start call (#7024)

* chore: format code and fix lint issues

* feat: Pre flight (#7038)

* action: organized translations

* feat: Receive voip push notifications from backend (#7045)

* feat: Refactor media session handling and improve disconnect logic (#7065)

* feat: Control incoming call from native (#7066)

* feat: Voice message blocks (#7057)

* feat: native accept success event (#7068)

* feat(voip): call waiting, busy detection, and videoconf blocking (#7077)

* action: organized translations

* feat(voip): tap-to-hide call controls with animations (#7078)

* feat(voip): navigate to call DM from message button and header (#7082)

* feat(voip): tablet and landscape layout (#7110)

* chore: develop into feat.voip-lib-new (RN 81 + Expo 54 + reanimated 4 + true-sheet + iOS 26) (#7114)

* chore: format code and fix lint issues

* feat(voip): android landscape layout for IncomingCallActivity (#7116)

* Update agents files

* feat(voip): Support a11y (#7106)

* Fix content cutting on iOS on some edge cases

* pods

* Ignore .worktrees on jest

* chore: Merge develop into feat.voip-lib-new (#7129)

* fix(voip): show CallKit UI when call is active in background (#7128)

* chore: Update media-signaling to 0.2.0 (#7153)

* feat(voip): migrate iOS accept/reject from DDP to REST (#7124)

* Fix icons

* feat(voip): migrate Android accept/reject from DDP to REST (#7127)

* test(voip): integration tests for CallView pipeline (#7161)

* feat(voip): display video conf provider as subtitle (#7160)

* fix(voip): CallView button grid and correct landscape/dialpad layouts (#7164)

* fix(voip): prevent stale MMKV cache on Android first-install accept

MMKVKeyManager.initialize ran in MainApplication.onCreate before the JS
engine started and opened the default MMKV file via the Tencent 1.2 JAR
when it was still empty. Tencent caches instances per-ID in a singleton
registry, so that empty-state view was held for the rest of the process.
JS later wrote credentials through react-native-mmkv (MMKV Core 2.0),
which has its own separate registry. When a VoIP push arrived,
Ejson.getMMKV() got the cached empty Tencent instance and reported
"No userId found in MMKV for server". Closing and reopening the app
cleared the cache, which is why only the very first call after install
failed.

Drop the open/verify block — the encryption key is already cached from
SecureKeystore, so no MMKV handle is needed here. The first Tencent
instance is now created inside Ejson.getMMKV() after JS has written,
so it scans the file fresh.

* fix(voip): prevent duplicate ringtone on Android incoming call (#7158)

* fix(voip): set explicit snaps for NewMediaCall bottom sheet (#7165)

* Update app/lib/services/voip/MediaSessionStore.ts

Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com>

* fix: make startVoipFork reactive to permissions-changed (#7151)

* fix(android): remove MediaProjectionService from merged manifest (#7190)

* fix(voip): Phone account creation (#7170)

* feat: add Enable Mobile Ringing toggle in user preferences (#7155)

* fix(voip): ship blockers for PushKit, licensing, outbound calls, push tokens (#7167)

* fix(android): Play Store mic discoverability, safer FCM logs, avatar auth via headers (#7171)

* fix(ios): serialize VoipService bridge statics (#7169)

* fix(voip): Android DDP thread safety and VoipPayload bundle parity (#7168)

* chore(voip): dead-code and hygiene sweep (#7174)

* refactor(voip): decouple navigateToCallRoom from Redux and backfill REST/connect tests (#7176)

* test(voip): tighten ringing endCall assertion and add VideoConf VoIP-lock saga coverage (#7177)

* fix(ios): harden VoIP DDP WebSocket client on receive failures and TLS (#7173)

* refactor(voip): MediaCallEvents Redux adapters and resetVoipState (#7178)

* refactor(voip): decouple peer autocomplete from Redux; simplify NewMediaCall (#7175)

* fix(ios): add NS_SWIFT_NAME to Challenge.runChallenge for Swift 6.2 compatibility

Swift 6.2 (Xcode 26.x / macos-26 runner) auto-renames the Objective-C
method runChallenge:didReceiveChallenge:completionHandler: to
run(_:didReceive:completionHandler:) when imported into Swift.

Add NS_SWIFT_NAME to explicitly pin the Swift import name, preventing
the compiler from applying its heuristics. This keeps the existing
Swift call site in DDPClient.swift working without changes.

* fix(ios): cancel old URLSession/webSocketTask before reconnecting in DDPClient.connect (#7197)

* fix(ios): add NSLock to nativeAcceptHandledCallIds and 10s REST timeout to handleNativeAccept (#7198)

* feat(android): create VoipCallService with FOREGROUND_SERVICE_MICROPHONE (#7199)

* fix(android): start VoipCallService on accept, stop on hangup/timeout, install end-call listener (#7200)

* fix(voip): enable DM nav for users with SIP extension (#7203)

* fix(android): handle null VoiceConnection in answerIncomingCall, notify JS (#7201)

* fix(voip): resolve closure capture ordering in handleNativeAccept (#7209)

* fix(android): integrate VoIP modules with SSL-pinned OkHttpClient (#7208)

* fix(push): gate id and voipToken behind server version checks, fix VideoConf caller extra (#7210)

* fix(voip): remove sensitive data from production logs (#7207)

* fix(android): remove isRunning guard + add double-tap guard on Accept/Decline

- VoipCallService: remove if (!isRunning) guard, call startForeground unconditionally
  (idempotent on Android, fixes Android 14+ foreground service requirement)
- IncomingCallActivity: add AtomicBoolean guard on handleAccept/handleDecline
  to prevent double-tap from triggering multiple service starts

---------

Co-authored-by: diegolmello <diegolmello@users.noreply.github.com>
Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant