Skip to content

fix(android/voip): start microphone FGS for outgoing calls so audio survives backgrounding#7346

Merged
diegolmello merged 4 commits into
developfrom
native-1178-fix
May 27, 2026
Merged

fix(android/voip): start microphone FGS for outgoing calls so audio survives backgrounding#7346
diegolmello merged 4 commits into
developfrom
native-1178-fix

Conversation

@diegolmello

@diegolmello diegolmello commented May 26, 2026

Copy link
Copy Markdown
Member

Proposed changes

Outgoing VoIP 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, Android revokes RECORD_AUDIO (while-in-use) ~5 s after the user backgrounds the app, dropping the caller's audio while the peer's still plays.

The incoming-accept path already starts VoipCallService from native after Telecom is active. This PR mirrors that behaviour for outgoing calls by:

  • Exposing startVoipCallService(callId): Promise<void> on the RNVoipModule TurboModule. The promise is held until VoipCallService.onStartCommand reports back via VoipModule.notifyFgsStarted / notifyFgsFailed, so async failures (ForegroundServiceTypeNotAllowedException, ForegroundServiceDidNotStartInTimeException, SecurityException at startForeground time) reach JS instead of being masked by a synchronous resolve(null). A 7-second timeout rejects with E_VOIP_FGS_TIMEOUT if the service never signals back. Synchronous failures from startForegroundService still reject with E_VOIP_FGS_START.
  • Invoking it Android-only from the newCall 'caller' branch in MediaSessionInstance.ts, while the initiating activity is still visible (so the microphone FGS-start eligibility is satisfied).
  • Gating Navigation.navigate('CallView') on the FGS promise resolving. On rejection JS tears the outgoing call down via endCall(callId) + VoIP_Call_Issue alert and stays on the current screen — previously the eager navigate stranded the user on a CallView whose 'ended' listener was detached by useCallStore.reset() before async hangup() could fire Navigation.back().
  • iOS VoipModule.mm gets a no-op resolve(nil) stub to satisfy the codegen protocol.

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 fix(android/voip): use phoneCall FGS type and fix inverted accept/decline guard #7233 rationale).
  • microphone is what actually authorises background mic capture — phoneCall alone keeps the call alive via ConnectionService but does not exempt RECORD_AUDIO from the while-in-use clock.

Both call sites reach startForeground() from a while-in-use eligible state: outgoing from a visible activity, incoming after connection.onAnswer() puts Telecom into STATE_ACTIVE. Doc note at app/lib/services/voip/docs/PLATFORMS.md was wrong and is corrected in the same commit.

Issue(s)

https://rocketchat.atlassian.net/browse/NATIVE-1178

How to test or reproduce

Android manual repro (outgoing audio drop):

  1. Start an outgoing VoIP call from the app.
  2. Confirm two-way audio works in the foreground.
  3. Background the app (home / recents) and continue speaking for at least 10 seconds.
  4. Confirm the peer continues to hear the caller's audio — before this fix, audio drops ~5 s after backgrounding.
  5. Foreground the app, confirm the call is still active and audio is restored both ways.
  6. End the call; verify the foreground service / notification is cleared.

Android manual repro (FGS-rejection no-strand):

  1. Force an FGS-start failure (e.g. background-restricted state on Android 12+ that triggers ForegroundServiceStartNotAllowedException, or temporarily revoke FOREGROUND_SERVICE_MICROPHONE in the manifest).
  2. Initiate an outgoing call.
  3. Confirm the user sees the VoIP_Call_Issue alert and stays on the originating screen — no blank CallView.

Screenshots

N/A — audio-only fix.

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

Five regression tests in MediaSessionInstance.test.ts: outgoing-caller starts the FGS on Android, incoming-callee does not, an FGS-start rejection tears down the call, navigation runs after FGS resolves, and navigation does NOT run on FGS rejection. The microphone FGS type is deliberately scoped to the two existing call sites (outgoing from visible activity, incoming after connection.onAnswer()); no new code path starts VoipCallService from a non-foreground context, preserving the PR #7233 fix for the SecurityException crash class.

Summary by CodeRabbit

  • New Features

    • Android: Outgoing VoIP calls now start a native foreground service and app declares microphone+phone-call foreground capability.
    • JS API: Added start-service method; iOS is a no-op placeholder. Foreground start is coordinated with JS and can time out.
  • Bug Fixes

    • Safer handling when foreground service startup fails: surfaces error to JS, shows alert, ends call, and prevents navigation.
  • Tests

    • Added tests for outgoing start flow and failure handling.
  • Documentation

    • Updated platform docs with foreground-service and permission timing details.

Review Change Stack

@diegolmello diegolmello temporarily deployed to approve_e2e_testing May 26, 2026 13:22 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4a49b8af-baf9-4ce0-b13d-ebc059df2337

📥 Commits

Reviewing files that changed from the base of the PR and between 066a4b5 and a5ffc4c.

📒 Files selected for processing (8)
  • 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/VoipModule.kt
  • app/lib/native/NativeVoip.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/services/voip/docs/PLATFORMS.md
  • ios/Libraries/VoipModule.mm

Walkthrough

Adds microphone to Android VoIP foreground-service types, introduces a JS → native startVoipCallService promise flow with success/failure/timeout handling, integrates native start into outgoing Android call startup, updates tests/docs, and adds an iOS no-op TurboModule method.

Changes

VoIP Foreground Service Permissions

Layer / File(s) Summary
Android manifest and service start
android/app/src/main/AndroidManifest.xml, android/app/src/main/java/chat/rocket/reactnative/voip/VoipCallService.kt
Manifest declares VoipCallService with `android:foregroundServiceType="microphone
Android native module promise coordination
android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt
Adds pending-start tracking by callId, notifyFgsStarted / notifyFgsFailed callbacks, supersede handling, start timeout, and startVoipCallService(callId, promise) which dispatches VoipCallService.startService(...).
TurboModule Spec and fallback
app/lib/native/NativeVoip.ts
Spec adds startVoipCallService(callId: string): Promise<void> and the fallback NativeVoipModule supplies a resolved-Promise stub.
Call session integration
app/lib/services/voip/MediaSessionInstance.ts
On Android outgoing caller newCall, calls NativeVoipModule.startVoipCallService(callId) before navigating; on failure logs/shows error and ends the call.
Tests for JS/native start behavior
app/lib/services/voip/MediaSessionInstance.test.ts
Jest mock adds startVoipCallService and stopVoipCallService; tests assert start on outgoing caller, skip on incoming callee, flush microtask before navigation assertion, and verify failure-path teardown when native start rejects.
Docs and iOS stub
app/lib/services/voip/docs/PLATFORMS.md, ios/Libraries/VoipModule.mm
Docs updated to require `microphone

Sequence Diagram

sequenceDiagram
  participant MediaSessionInstance
  participant Platform
  participant NativeVoipModule
  participant VoipModule
  participant VoipCallService
  MediaSessionInstance->>Platform: isAndroid()
  alt Android && outgoing caller
    MediaSessionInstance->>NativeVoipModule: startVoipCallService(callId)
    NativeVoipModule->>VoipModule: startVoipCallService(callId, promise)
    VoipModule->>VoipCallService: startService(callId)
    VoipCallService->>VoipCallService: startForegroundWithNotification(type: MICROPHONE|PHONE_CALL)
    VoipCallService-->>VoipModule: notifyFgsStarted(callId) / notifyFgsFailed(callId, throwable)
    VoipModule-->>NativeVoipModule: resolve / reject promise
    NativeVoipModule-->>MediaSessionInstance: promise resolved / rejected
  else not Android or not caller
    MediaSessionInstance->>MediaSessionInstance: continue without native start
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • RocketChat/Rocket.Chat.ReactNative#7346: Overlapping coordinated changes adding startVoipCallService TurboModule, gating Android outgoing navigation on FGS start, and updating VoipCallService/AndroidManifest foreground types.

Suggested reviewers

  • OtavioStasiak
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 accurately and concisely summarizes the primary change: enabling microphone foreground service for outgoing VoIP calls on Android to preserve audio when backgrounded.
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.

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • NATIVE-1178: Request failed with status code 401

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.

🧹 Nitpick comments (1)
app/lib/services/voip/docs/PLATFORMS.md (1)

71-71: ⚡ Quick win

Consider expanding the FSI acronym for clarity.

The term "FSI" refers to FullScreenIntent, which may not be immediately familiar to all readers of this documentation. Consider expanding it on first use for improved clarity.

📝 Suggested clarification
-  - `phoneCall` satisfies FGS-**start** eligibility on the incoming-accept path. That path starts the service from `VoipNotification.handleAcceptAction` _after_ `connection.onAnswer()` puts the self-managed Telecom call into `STATE_ACTIVE`, which is what makes `phoneCall` startable from a non-foreground entry point (FSI / `onNewIntent`).
+  - `phoneCall` satisfies FGS-**start** eligibility on the incoming-accept path. That path starts the service from `VoipNotification.handleAcceptAction` _after_ `connection.onAnswer()` puts the self-managed Telecom call into `STATE_ACTIVE`, which is what makes `phoneCall` startable from a non-foreground entry point (FullScreenIntent / `onNewIntent`).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/services/voip/docs/PLATFORMS.md` at line 71, In the sentence
referencing "FSI / `onNewIntent`", expand the acronym FSI to "FullScreenIntent"
on first use (e.g., "FSI (FullScreenIntent)") so readers understand the term;
update the documentation near the `phoneCall` /
`VoipNotification.handleAcceptAction` / `connection.onAnswer()` / `STATE_ACTIVE`
/ `onNewIntent` discussion to use the full form on first mention and you can
keep the acronym thereafter.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/lib/services/voip/docs/PLATFORMS.md`:
- Line 71: In the sentence referencing "FSI / `onNewIntent`", expand the acronym
FSI to "FullScreenIntent" on first use (e.g., "FSI (FullScreenIntent)") so
readers understand the term; update the documentation near the `phoneCall` /
`VoipNotification.handleAcceptAction` / `connection.onAnswer()` / `STATE_ACTIVE`
/ `onNewIntent` discussion to use the full form on first mention and you can
keep the acronym thereafter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4326d72e-848d-4f97-856e-dc30e0a77917

📥 Commits

Reviewing files that changed from the base of the PR and between 612dd1a and a0b8fd7.

📒 Files selected for processing (7)
  • 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/VoipModule.kt
  • app/lib/native/NativeVoip.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/services/voip/docs/PLATFORMS.md
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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.test.ts
  • app/lib/native/NativeVoip.ts
  • app/lib/services/voip/MediaSessionInstance.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{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

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

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use Prettier with tabs, single quotes, 130 char width, no trailing commas, arrow parens avoid, bracket same line
Use @rocket.chat/eslint-config base with React, React Native, TypeScript, Jest plugins

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

VoIP service implementation should use Zustand stores (not Redux) and include native CallKit (iOS) and Telecom (Android) integration in app/lib/services/voip/

Files:

  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/services/voip/MediaSessionInstance.ts
🧠 Learnings (1)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/native/NativeVoip.ts
  • app/lib/services/voip/MediaSessionInstance.ts
🔇 Additional comments (6)
android/app/src/main/AndroidManifest.xml (1)

156-161: LGTM!

Also applies to: 166-166

android/app/src/main/java/chat/rocket/reactnative/voip/VoipCallService.kt (1)

103-109: LGTM!

Also applies to: 113-113

android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt (1)

174-186: LGTM!

app/lib/native/NativeVoip.ts (1)

38-47: LGTM!

Also applies to: 103-103

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

11-11: LGTM!

Also applies to: 17-17, 133-144

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

115-115: LGTM!

Also applies to: 118-121, 419-455

@github-actions

Copy link
Copy Markdown

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
app/lib/native/NativeVoip.ts (1)

96-105: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Make the fallback fail closed on Android.

If TurboModuleRegistry.get('VoipModule') returns null on Android, this stub resolves and the caller will keep the outgoing call alive even though no FGS was started. That bypasses the new teardown path this PR adds for FGS-start failures. Please reject here on Android instead of silently succeeding, and make the stub signature explicit while you’re touching it.

♻️ Proposed fix
-import { TurboModuleRegistry } from 'react-native';
+import { Platform, TurboModuleRegistry } from 'react-native';
...
-		startVoipCallService: () => Promise.resolve(),
+		startVoipCallService: (_callId: string): Promise<void> =>
+			Platform.OS === 'android'
+				? Promise.reject(new Error('VoipModule.startVoipCallService is unavailable on Android'))
+				: Promise.resolve(),

As per coding guidelines **/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/native/NativeVoip.ts` around lines 96 - 105, The fallback
NativeVoipModule stub currently silently succeeds when
TurboModuleRegistry.get('VoipModule') returns null, which keeps outgoing calls
alive on Android; change the stub so on Android (Platform.OS === 'android')
startVoipCallService returns a rejected Promise (e.g. Promise.reject(new
Error('VoipModule unavailable'))) to fail closed and propagate the error to the
caller, and add explicit TypeScript signatures for the stubbed methods
(registerVoipToken, getInitialEvents, clearInitialEvents, getLastVoipToken,
stopNativeDDPClient, startVoipCallService, stopVoipCallService) to match the
Spec interface for type safety.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/lib/services/voip/MediaSessionInstance.ts`:
- Around line 140-145: The Android FGS start is async so you must gate the rest
of the outgoing-call setup on a successful start: move the navigation to
CallView and the DM/room lookup logic currently running after the
startVoipCallService call into the success path (the .then / resolved branch) of
NativeVoipModule.startVoipCallService so they only run when the FGS starts; keep
the existing .catch to log, show the error alert and call
this.endCall(call.callId) on failure. Ensure you reference
NativeVoipModule.startVoipCallService, the code that navigates to CallView, and
the room/DM resolution logic and wrap them in the success handler so a rejected
start remains a clean failure.

---

Outside diff comments:
In `@app/lib/native/NativeVoip.ts`:
- Around line 96-105: The fallback NativeVoipModule stub currently silently
succeeds when TurboModuleRegistry.get('VoipModule') returns null, which keeps
outgoing calls alive on Android; change the stub so on Android (Platform.OS ===
'android') startVoipCallService returns a rejected Promise (e.g.
Promise.reject(new Error('VoipModule unavailable'))) to fail closed and
propagate the error to the caller, and add explicit TypeScript signatures for
the stubbed methods (registerVoipToken, getInitialEvents, clearInitialEvents,
getLastVoipToken, stopNativeDDPClient, startVoipCallService,
stopVoipCallService) to match the Spec interface for type safety.
🪄 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: b7c7c298-3b1d-4df0-88a1-54024f8c931b

📥 Commits

Reviewing files that changed from the base of the PR and between a0b8fd7 and 1f600ba.

📒 Files selected for processing (6)
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt
  • app/lib/native/NativeVoip.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/services/voip/docs/PLATFORMS.md
  • ios/Libraries/VoipModule.mm
✅ Files skipped from review due to trivial changes (1)
  • app/lib/services/voip/docs/PLATFORMS.md
📜 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). (2)
  • GitHub Check: ESLint and Test / run-eslint-and-test
  • GitHub Check: format
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/native/NativeVoip.ts
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{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

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

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use Prettier with tabs, single quotes, 130 char width, no trailing commas, arrow parens avoid, bracket same line
Use @rocket.chat/eslint-config base with React, React Native, TypeScript, Jest plugins

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

VoIP service implementation should use Zustand stores (not Redux) and include native CallKit (iOS) and Telecom (Android) integration in app/lib/services/voip/

Files:

  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
🧠 Learnings (3)
📚 Learning: 2026-03-05T06:06:12.277Z
Learnt from: divyanshu-patil
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6957
File: ios/RCTWatchModule.mm:19-24
Timestamp: 2026-03-05T06:06:12.277Z
Learning: Do not re-activate or reset the WCSession singleton in iOS Objective-C/Swift bridge modules. Ensure WCSession is activated and its delegate is set in a single, central place (e.g., ios/RocketChat Watch App/Loaders/WatchSession.swift) and avoid duplicating activation or delegate assignment in other iOS bridge files like ios/RCTWatchModule.mm. If WCSession is already activated via the central loader, relying on WCSession.defaultSession is sufficient and maintains a single session lifecycle.

Applied to files:

  • ios/Libraries/VoipModule.mm
📚 Learning: 2026-05-12T21:58:10.053Z
Learnt from: Rohit3523
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6991
File: ios/Libraries/Challenge.mm:29-37
Timestamp: 2026-05-12T21:58:10.053Z
Learning: For iOS code using react-native-mmkv, when calling `[MMKV initializeMMKV:nil groupDir:groupDir logLevel:...]` (and when passing `rootPath` to MMKVBridge), pass the raw App Group container URL path only (e.g., `[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroup] path]`). Do NOT append `/mmkv` yourself. The MMKV SDK already appends `/mmkv` internally (`...stringByAppendingPathComponent:@"mmkv"`), so passing `<AppGroup>/mmkv` will cause double-nesting (`<AppGroup>/mmkv/mmkv`) and can break shared storage/SSL cert lookups.

Applied to files:

  • ios/Libraries/VoipModule.mm
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/lib/native/NativeVoip.ts
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
🔇 Additional comments (3)
ios/Libraries/VoipModule.mm (1)

120-125: LGTM!

android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt (1)

174-189: LGTM!

app/lib/native/NativeVoip.ts (1)

38-47: LGTM!

Comment thread app/lib/services/voip/MediaSessionInstance.ts Outdated

@diegolmello diegolmello left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Single inline finding from a structural review — otherwise this PR is clean (focused, no spaghetti added to unrelated flows, no file pushed past 1k lines, TurboModule contract is explicit, failure path correctly tears down rather than silently degrading).

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.73.0.108966

@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)
app/lib/native/NativeVoip.ts (1)

99-116: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject when VoipModule is unavailable on Android.

startVoipCallService() is now the success gate for outgoing-call setup. If TurboModuleRegistry.get('VoipModule') returns null on Android, this fallback resolves and the app proceeds as if the FGS started, which masks the broken native integration and reintroduces the background mic-drop path instead of failing fast.

Proposed fix
 import type { TurboModule } from 'react-native';
-import { TurboModuleRegistry } from 'react-native';
+import { Platform, TurboModuleRegistry } from 'react-native';
@@
 		getInitialEvents: () => null,
 		clearInitialEvents: () => undefined,
 		getLastVoipToken: () => '',
 		stopNativeDDPClient: () => undefined,
-		startVoipCallService: () => Promise.resolve(),
+		startVoipCallService: () =>
+			Platform.OS === 'android'
+				? Promise.reject(new Error('VoipModule is unavailable on Android'))
+				: Promise.resolve(),
 		stopVoipCallService: () => undefined,
 		setSpeakerOn: () => Promise.resolve(false),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/native/NativeVoip.ts` around lines 99 - 116, The fallback for
TurboModuleRegistry.get('VoipModule') silently returns no-op implementations
(NativeVoipModule) which makes startVoipCallService succeed on Android when the
native module is missing; change the fallback so that missing module causes
failures: when TurboModuleRegistry.get<Spec>('VoipModule') is null/undefined,
replace the no-op object with an implementation whose startVoipCallService (and
any other critical async startup methods like startAudioRouteSync/startRingback)
return a rejected Promise (or otherwise throw) indicating the VoipModule is
unavailable, and ensure getInitialEvents/getLastVoipToken behave safely but do
not hide the missing-module error so callers of
NativeVoipModule.startVoipCallService see a rejection instead of a resolved
Promise.
♻️ Duplicate comments (1)
app/lib/services/voip/MediaSessionInstance.ts (1)

141-150: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Gate the DM lookup on FGS success as well.

Navigation.navigate() is gated now, but resolveRoomIdFromContact() still starts before the FGS promise settles. If startVoipCallService() rejects, endCall() resets the store and the in-flight lookup can write a stale roomId back afterward.

Proposed fix
 					if (Platform.OS === 'android') {
 						NativeVoipModule.startVoipCallService(call.callId)
-							.then(() => Navigation.navigate('CallView'))
+							.then(() => {
+								Navigation.navigate('CallView');
+								if (useCallStore.getState().roomId == null) {
+									this.resolveRoomIdFromContact(call.remoteParticipants[0]?.contact).catch(error => {
+										log(error);
+									});
+								}
+							})
 							.catch(error => {
 								log(error);
 								showErrorAlert(I18n.t('VoIP_Call_Issue'), I18n.t('Oops'));
 								this.endCall(call.callId);
 							});
 					} else {
 						Navigation.navigate('CallView');
-					}
-					if (useCallStore.getState().roomId == null) {
-						this.resolveRoomIdFromContact(call.remoteParticipants[0]?.contact).catch(error => {
-							log(error);
-						});
+						if (useCallStore.getState().roomId == null) {
+							this.resolveRoomIdFromContact(call.remoteParticipants[0]?.contact).catch(error => {
+								log(error);
+							});
+						}
 					}
 				}

Also applies to: 152-156

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/services/voip/MediaSessionInstance.ts` around lines 141 - 150, The
room ID lookup (resolveRoomIdFromContact) must be deferred until the
FGS/startVoipCallService promise settles to avoid stale writes if
startVoipCallService rejects; modify the logic around
NativeVoipModule.startVoipCallService(call.callId) and the non-Android branch so
that you only call resolveRoomIdFromContact(...) and
Navigation.navigate('CallView') after startVoipCallService() resolves
successfully, and if it rejects call this.endCall(call.callId) and do not run
the lookup or navigate; ensure you reference the existing methods
startVoipCallService, resolveRoomIdFromContact, endCall and Navigation.navigate
when moving the lookup to the success path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@app/lib/native/NativeVoip.ts`:
- Around line 99-116: The fallback for TurboModuleRegistry.get('VoipModule')
silently returns no-op implementations (NativeVoipModule) which makes
startVoipCallService succeed on Android when the native module is missing;
change the fallback so that missing module causes failures: when
TurboModuleRegistry.get<Spec>('VoipModule') is null/undefined, replace the no-op
object with an implementation whose startVoipCallService (and any other critical
async startup methods like startAudioRouteSync/startRingback) return a rejected
Promise (or otherwise throw) indicating the VoipModule is unavailable, and
ensure getInitialEvents/getLastVoipToken behave safely but do not hide the
missing-module error so callers of NativeVoipModule.startVoipCallService see a
rejection instead of a resolved Promise.

---

Duplicate comments:
In `@app/lib/services/voip/MediaSessionInstance.ts`:
- Around line 141-150: The room ID lookup (resolveRoomIdFromContact) must be
deferred until the FGS/startVoipCallService promise settles to avoid stale
writes if startVoipCallService rejects; modify the logic around
NativeVoipModule.startVoipCallService(call.callId) and the non-Android branch so
that you only call resolveRoomIdFromContact(...) and
Navigation.navigate('CallView') after startVoipCallService() resolves
successfully, and if it rejects call this.endCall(call.callId) and do not run
the lookup or navigate; ensure you reference the existing methods
startVoipCallService, resolveRoomIdFromContact, endCall and Navigation.navigate
when moving the lookup to the success path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6dca8a43-46d9-4224-afd1-462c23a17ff6

📥 Commits

Reviewing files that changed from the base of the PR and between 1f600ba and 066a4b5.

📒 Files selected for processing (6)
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipCallService.kt
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt
  • app/lib/native/NativeVoip.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/services/voip/docs/PLATFORMS.md
✅ Files skipped from review due to trivial changes (1)
  • app/lib/services/voip/docs/PLATFORMS.md
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/native/NativeVoip.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{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

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

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use Prettier with tabs, single quotes, 130 char width, no trailing commas, arrow parens avoid, bracket same line
Use @rocket.chat/eslint-config base with React, React Native, TypeScript, Jest plugins

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

VoIP service implementation should use Zustand stores (not Redux) and include native CallKit (iOS) and Telecom (Android) integration in app/lib/services/voip/

Files:

  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
🧠 Learnings (1)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/native/NativeVoip.ts

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.73.0.108970

@OtavioStasiak OtavioStasiak 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.

LGTM!
Tested on both platforms.

diegolmello and others added 4 commits May 27, 2026 11:50
…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.
…outgoing call

Continuing without an FGS reproduces the silent mic-drop bug this PR exists to
prevent. Make startVoipCallService return Promise<void>; on rejection JS calls
endCall and shows VoIP_Call_Issue so the user sees a real failure instead of a
degraded call.
… nav on success

VoipCallService now reports back via VoipModule.notifyFgsStarted / notifyFgsFailed
once startForeground actually returns, so async failures
(ForegroundServiceTypeNotAllowedException, SecurityException at startForeground
time) reach JS instead of being masked by a synchronous resolve. A 7s timeout
rejects with E_VOIP_FGS_TIMEOUT if the service never signals back.

MediaSessionInstance gates Navigation.navigate('CallView') on the FGS promise
resolving on Android. On rejection it stays on the current screen and tears the
call down — previously the catch handler would alert + endCall but the eager
navigate had already happened, stranding the user on a CallView whose 'ended'
listener was detached by useCallStore.reset() before async hangup() could fire
Navigation.back().
@diegolmello diegolmello requested a deployment to approve_e2e_testing May 27, 2026 14:51 — with GitHub Actions Waiting
@diegolmello diegolmello merged commit 41444f4 into develop May 27, 2026
5 of 7 checks passed
@diegolmello diegolmello deleted the native-1178-fix branch May 27, 2026 14:51
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.

2 participants