Skip to content

feat(android): create VoipCallService with FOREGROUND_SERVICE_MICROPHONE#7199

Merged
diegolmello merged 2 commits into
feat.voip-lib-newfrom
feat/voip-android-call-service
Apr 22, 2026
Merged

feat(android): create VoipCallService with FOREGROUND_SERVICE_MICROPHONE#7199
diegolmello merged 2 commits into
feat.voip-lib-newfrom
feat/voip-android-call-service

Conversation

@diegolmello

@diegolmello diegolmello commented Apr 22, 2026

Copy link
Copy Markdown
Member

Proposed changes

Create VoipCallService — an Android foreground Service with foregroundServiceType="microphone" — so VoIP audio calls keep running when the app is backgrounded. Without a foreground service, Android terminates the process and drops the active WebRTC audio session, which is the same problem iOS solves via CallKit audio retention.

What this adds

  • VoipCallService.kt — foreground service that starts/stops around the call lifecycle.
    • startService(context, callId): starts in foreground with an ongoing notification.
    • stopService(context): requests shutdown.
    • ACTION_START / ACTION_STOP intent actions for explicit control.
    • ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE on API 29+.
    • Low-priority notification channel and ongoing notification.
  • AndroidManifest.xml — declares the service with foregroundServiceType="microphone", enabled="true", exported="false".

Review fixes applied

Addresses CodeRabbit review findings #1 (production crash) and #2 (start-command semantics):

  1. stopService() helper now calls context.stopService(intent) instead of context.startService(intent). On Android 8+ (API 26+), invoking startService from a background context throws IllegalStateException / BackgroundServiceStartNotAllowedException — which is exactly when a hangup from a backgrounded or headless-notification flow happens.
  2. onStartCommand returns START_NOT_STICKY for ACTION_START (was START_STICKY). A redelivered null intent cannot recover the WebRTC peer connection, so sticky semantics would only log a warning and terminate anyway.
  3. stopSelf(startId) replaces stopSelf() in the ACTION_STOP and unknown-action branches, so only the matching start token is released and no start counts are leaked.

Issue(s)

N/A — internal VoIP work.

How to test or reproduce

  1. Place a VoIP call (ACTION_START), background the app, verify audio continues and the ongoing notification is visible.
  2. Hang up from the notification (ACTION_STOP); verify the service stops without IllegalStateException in logs.
  3. Send ACTION_STOP while the app is fully backgrounded; the service still tears down cleanly (this is the case the fix unblocks).

Types of changes

  • New feature (non-breaking change which adds functionality)
  • Bugfix (non-breaking change which fixes an issue) — for the review-fix commit on top

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 — N/A for foreground service lifecycle; covered by manual VoIP test plan above
  • I have added necessary documentation — N/A
  • Any dependent changes have been merged and published in downstream modules

Further comments

Merge order

This is PR 3 of 6 in the VoIP Android integration series:

  1. PR-2: fix(ios) DDP cleanup — independent (merged)
  2. PR-1: fix(ios) NSLock + timeout — independent (merged)
  3. PR-3: feat(android) VoipCallService ← this PR
  4. PR-4: fix(android) service integration — depends on PR-3
  5. PR-5: fix(both) null guard — independent, merge before PR-6
  6. PR-6: chore(ts) cleanup — last (shares file with PR-5)

Summary by CodeRabbit

  • New Features
    • VoIP calling now runs reliably in the background, allowing calls to persist when switching apps.
    • Active calls show a persistent notification so users are informed and can return to the app quickly.

New `VoipCallService` extends `Service` and runs as a foreground service with
`foregroundServiceType="microphone"`. Keeps VoIP audio calls alive when the app moves
to the background (Android terminates background processes without a foreground service).

- Service starts via `VoipCallService.startService(context, callId)` with ACTION_START
- Service stops via `VoipCallService.stopService(context)` with ACTION_STOP
- Starts with `ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE` on API 29+
- Creates low-priority notification channel and ongoing notification
- Manifest declares service with `foregroundServiceType="microphone"`

Fixes: C2 (missing VoipCallService with FOREGROUND_SERVICE_MICROPHONE)
@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0ebfdb2b-f542-49a3-a4d0-3d9253991606

📥 Commits

Reviewing files that changed from the base of the PR and between 359f1bf and 4d9feb2.

📒 Files selected for processing (1)
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipCallService.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipCallService.kt

Walkthrough

Adds a new Android foreground service, VoipCallService, to manage VoIP calls in the background; it is declared in the manifest with microphone foreground service type and provides intent-based start/stop entry points, notification handling, and lifecycle management.

Changes

Cohort / File(s) Summary
Android Manifest Configuration
android/app/src/main/AndroidManifest.xml
Added a new <service> entry for chat.rocket.reactnative.voip.VoipCallService with android:enabled="true", android:exported="false", and android:foregroundServiceType="microphone".
VoIP Call Service Implementation
android/app/src/main/java/chat/rocket/reactnative/voip/VoipCallService.kt
New foreground Service class with companion helpers startService(context, callId) and stopService(context) that send start/stop intents; handles ACTION_START/ACTION_STOP, prevents duplicate starts via isRunning, creates notification channel (O+), builds ongoing notification with PendingIntent to MainActivity, starts foreground (uses FOREGROUND_SERVICE_TYPE_MICROPHONE on Q+), and cleans up on destroy.

Sequence Diagram(s)

sequenceDiagram
    participant App as App / Caller
    participant Service as VoipCallService
    participant Notif as NotificationManager
    participant OS as Android OS
    participant Main as MainActivity
    participant User as User

    App->>Service: startService(context, callId)
    Service->>Service: if isRunning -> ignore
    alt not running
        Service->>Notif: createNotificationChannel() (Android O+)
        Service->>Notif: build ongoing Notification (PendingIntent -> Main)
        Service->>OS: startForeground(notification) [FOREGROUND_SERVICE_TYPE_MICROPHONE on Q+]
        Service->>Service: isRunning = true
    end

    User->>Main: tap notification (PendingIntent)

    App->>Service: stopService(context)
    Service->>Service: onStartCommand(ACTION_STOP) -> stopSelf()
    Service->>OS: stopForeground/stopSelf()
    Service->>Service: onDestroy() -> isRunning = false
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

type: feature

🚥 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 'feat(android): create VoipCallService with FOREGROUND_SERVICE_MICROPHONE' accurately and specifically describes the main change: creation of a new Android VoIP service with foreground microphone capability.
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 (6)
  • PR-2: Request failed with status code 401
  • PR-1: Request failed with status code 401
  • PR-3: Request failed with status code 401
  • PR-4: Request failed with status code 401
  • PR-5: Request failed with status code 401
  • PR-6: 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.

@diegolmello

Copy link
Copy Markdown
Member Author

Code Review — PR #7199

Files Reviewed: VoipCallService.kt, AndroidManifest.xml
Issues: 0 CRITICAL/HIGH, 2 LOW observations


[LOW] isRunning flag is static but also managed via service lifecycle

File: VoipCallService.kt:26

isRunning is a static flag used to prevent duplicate startForeground calls. However, since each startService() call with ACTION_START creates a new startId, the START_STICKY return value means the service could be killed and recreated. The isRunning flag won't be reset on service recreation (it remains true), which means subsequent startService calls would be silently dropped.

Fix: Remove the isRunning guard entirely — startForeground() is idempotent and can be called multiple times on the same service instance. If the service is killed and recreated, Android recreates it in a fresh state.

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    when (intent?.action) {
        ACTION_STOP -> {
            Log.d(TAG, "Stopping VoipCallService")
            stopSelf()
            return START_NOT_STICKY
        }
        ACTION_START -> {
            val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "unknown"
            Log.d(TAG, "Starting VoipCallService for callId: $callId")
            startForegroundWithNotification(callId)
            return START_STICKY
        }
        else -> {
            Log.w(TAG, "Unknown action: ${intent?.action}")
            return START_NOT_STICKY
        }
    }
}

[LOW] Small icon fallback uses application icon which may not be visible

File: VoipCallService.kt:99

getApplicationInfo().icon falls back to the app's launcher icon, which is typically not designed for notification small icons (expects a flat, monochrome drawable). On Android 5-6, this renders as a white silhouette. Consider creating a dedicated ic_voip_call drawable.

This is LOW because: (a) the notification is PRIORITY_LOW and not user-facing, (b) the fallback is standard Android practice, (c) CallKit/Telecom keeps the call alive anyway.


Acceptance Criteria Verification

Criterion Status Notes
VoipCallService.kt exists and compiles BUILD SUCCESSFUL
Manifest declares service with foregroundServiceType="microphone" Line 152-156
Manifest declares FOREGROUND_SERVICE_MICROPHONE permission Implied by foregroundServiceType attribute (no separate permission needed on API 34-)
Service starts in foreground with notification startForegroundWithNotification() with ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE on API 29+
Service stops via stopSelf() correctly ACTION_STOP path calls stopSelf() at line 79

Positive Observations

  1. Clean intent-based control. ACTION_START/ACTION_STOP pattern is simple, testable, and avoids static state.
  2. API-level guard correct. ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE only used on API 29+ (Android 10), with plain startForeground on earlier versions.
  3. Notification channel properly scoped. IMPORTANCE_LOW + setShowBadge(false) is correct for a persistent service notification that shouldn't interrupt the user.
  4. PendingIntent flags correct. Uses FLAG_IMMUTABLE on API 31+ with fallback to 0.
  5. Matches iOS CallKit pattern. Parity with how CallKit keeps the VoIP call alive on iOS.

Verdict

LGTM — All 5 acceptance criteria satisfied. Both LOW observations are informational only and don't block the PR. The isRunning guard issue is LOW because in practice, startForeground is idempotent even if the flag isn't reset on recreation.

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

🤖 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 73-74: The code logs a potentially sensitive call identifier when
starting the service; in VoipCallService replace the Log.d(TAG, "Starting
VoipCallService for callId: $callId") with a generic lifecycle message that
omits the callId (e.g., "Starting VoipCallService"); keep retrieving
EXTRA_CALL_ID into callId if needed for logic but do not include it in any logs
or exceptions, and make the same change for the other occurrence that logs
callId (the Log.d/Log.i call around the second usage noted at the other
occurrence).
- Around line 50-56: The stopService function in VoipCallService builds an
Intent with ACTION_STOP and incorrectly calls context.startService(intent);
change this to call context.stopService(intent) so the service is stopped via
Context.stopService(), avoiding background-start IllegalStateException on
Android 8+; update the call inside fun stopService(context:
android.content.Context) to use context.stopService(intent) while keeping the
Intent construction with ACTION_STOP and the VoipCallService::class.java target.
- Around line 65-85: The onStartCommand implementation in VoipCallService should
not return START_STICKY because the service cannot recover call state from a
null intent; update the ACTION_START branch return to START_NOT_STICKY so the OS
won't restart the service expecting to restore an active WebRTC call, and change
unconditional stopSelf() calls to stopSelf(startId) to use the precise lifecycle
token; specifically modify VoipCallService.onStartCommand (handling ACTION_STOP
and ACTION_START), keep the existing isRunning and
startForegroundWithNotification(callId) logic, but replace returns of
START_STICKY with START_NOT_STICKY and replace stopSelf() with
stopSelf(startId).
🪄 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: 849f03af-c8b1-4ac9-bb62-a4f316ce8da8

📥 Commits

Reviewing files that changed from the base of the PR and between d30c291 and 359f1bf.

📒 Files selected for processing (2)
  • android/app/src/main/AndroidManifest.xml
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipCallService.kt
📜 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
🧠 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/AndroidManifest.xml
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipCallService.kt
🔇 Additional comments (1)
android/app/src/main/AndroidManifest.xml (1)

150-155: LGTM — service declaration matches the foreground microphone use case.

The service is non-exported and declares the expected microphone foreground service type.

@diegolmello

Copy link
Copy Markdown
Member Author

Code Review — Opus-Level Architecture Review

PR: #7199 — feat(android): create VoipCallService with FOREGROUND_SERVICE_TYPE_MICROPHONE
Files Reviewed: 2 (VoipCallService.kt, AndroidManifest.xml)
Total Issues: 3 (0 CRITICAL, 1 HIGH, 2 MEDIUM, 0 LOW)


Stage 1 — Spec Compliance

Acceptance Criterion Status
VoipCallService.kt exists and compiles PASS
Manifest declares service with foregroundServiceType=\"microphone\" PASS
Manifest declares FOREGROUND_SERVICE_MICROPHONE permission PASS
Service starts in foreground with notification PASS
Service stops via stopSelf() correctly PASS

All five acceptance criteria are satisfied.


Stage 2 — Code Quality

[HIGH] Race condition in isRunning flag on ACTION_START

File: VoipCallService.kt:65-70

if (!isRunning) {
    isRunning = true
    startForegroundWithNotification(callId)
} else {
    Log.d(TAG, \"Service already running, skipping duplicate start\")
}

The read of isRunning on line 65 and the write on line 66 are not atomic. If two ACTION_START intents arrive in quick succession — or if the service is recreated by the system between onStartCommand calls — the duplicate-start guard can be bypassed. Specifically, onCreate() resets isRunning = false, so a system-recreated instance would re-enter startForegroundWithNotification on the next ACTION_START.

Additionally, the isRunning flag lives in the companion object (JVM class-level state), meaning all instances of the service share the same flag. Android can instantiate the service multiple times between startForegroundService and onDestroy, making the flag unreliable as an instance-level guard.

Fix: Move the guard inside startForegroundWithNotification as a private instance field:

private var isForeground = false

private fun startForegroundWithNotification(callId: String) {
    if (isForeground) return
    isForeground = true
    // ... existing logic
}

Remove the companion object's isRunning field entirely.


[MEDIUM] getApplicationInfo().icon may not be the correct notification icon

File: VoipCallService.kt:131

.setSmallIcon(getApplicationInfo().icon)

This uses the app's main launcher icon. On Android 8+ (API 26+), notification icons must be monochrome (alpha-only). The launcher icon typically does not meet this requirement and will render as a grey silhouette on modern Android.

VoipNotification.kt:834-835 in the same package handles this correctly:

val smallIconResId = context.resources.getIdentifier(\"ic_notification\", \"drawable\", packageName)
// ...
setSmallIcon(smallIconResId)

Fix: Use the same resource lookup pattern as VoipNotification.kt, or reference a specific drawable resource by ID if ic_notification does not exist:

val smallIconResId = context.resources.getIdentifier(
    \"ic_notification\", \"drawable\", context.packageName
)
setSmallIcon(if (smallIconResId != 0) smallIconResId else getApplicationInfo().icon)

[MEDIUM] ACTION_STOP via startService is a no-op with an unnecessary service instantiation

File: VoipCallService.kt:50-56

@JvmStatic
fun stopService(context: android.content.Context) {
    val intent = Intent(context, VoipCallService::class.java).apply {
        action = ACTION_STOP
    }
    context.startService(intent)
}

When stopService is called and the service is not running, Android will still instantiate the service and call onCreate (which creates the notification channel), then onStartCommand with ACTION_STOP, then stopSelf(). This creates a brief, wasteful service lifecycle for a no-op.

Fix: Track service state externally and guard the call:

@JvmStatic
fun stopService(context: android.content.Context) {
    if (!isRunning) return
    val intent = Intent(context, VoipCallService::class.java).apply {
        action = ACTION_STOP
    }
    context.startService(intent)
}

(The isRunning flag in the companion object is already being used; it just needs to be moved out of the ACTION_START guard to be checked here too.)


Positive Observations

  • API level guards are correct. FOREGROUND_SERVICE_TYPE_MICROPHONE gated behind Build.VERSION_CODES.Q (API 29+). FLAG_IMMUTABLE gated behind Build.VERSION_CODES.M (API 23+). Both match Android platform requirements.
  • stopSelf() on ACTION_STOP is the correct self-managed shutdown pattern — no external coordination needed.
  • onDestroy resets isRunning — prevents stale state if the service is force-killed and a pending intent is delivered.
  • Notification channel uses IMPORTANCE_LOW — appropriate for a persistent service indicator that should not be intrusive.
  • setOngoing(true) + setOnlyAlertOnce(true) — correct for a persistent call notification.
  • Manifest service declaration is correctenabled=\"true\", exported=\"false\", foregroundServiceType=\"microphone\".
  • Pre-existing FOREGROUND_SERVICE_MICROPHONE permission at AndroidManifest.xml:17 — no regression.

Verdict: REQUEST CHANGES

The HIGH race condition is the blocking issue. The MEDIUM icon issue is worth fixing now for consistency. Once those two are addressed, this PR is ready to merge.

- Companion stopService() now calls Context.stopService() instead of
  startService(): calling startService() from a background context on
  Android 8+ throws IllegalStateException. stopService() with the same
  Intent is always safe and also respects the service component.
- onStartCommand now returns START_NOT_STICKY for ACTION_START so the
  system does not redeliver a null Intent after process death (which
  could not recover the WebRTC call state anyway).
- ACTION_STOP uses stopSelf(startId) so only the matching start token
  is released; the unknown-action branch also calls stopSelf(startId)
  before returning to prevent leaked start counts.
@diegolmello diegolmello had a problem deploying to experimental_android_build April 22, 2026 14:08 — with GitHub Actions Failure
@diegolmello diegolmello had a problem deploying to official_android_build April 22, 2026 14:08 — with GitHub Actions Failure
@diegolmello diegolmello had a problem deploying to experimental_ios_build April 22, 2026 14:08 — with GitHub Actions Failure
@diegolmello

Copy link
Copy Markdown
Member Author

Follow-up review — partially dismissing the prior Opus review

Re: previous review. Re-evaluated each finding against the current tree and Android platform guarantees. Summary: one valid minor, one obsolete, one overstated.

[HIGH] isRunning race condition — overstated, not a bug

The review claims the read/write on isRunning at VoipCallService.kt:75-76 is non-atomic and that the service can be instantiated multiple times in parallel.

Platform guarantees contradict this:

  • onStartCommand is invoked on the main thread (see Service docs). Back-to-back ACTION_START intents are serialized — no interleaved read/write is possible.
  • Android guarantees at most one instance of a given Service class per process (Services overview). The "multiple instances share the companion flag" scenario does not occur.
  • On process death the JVM class reloads and the companion field reinitializes to false — the explicit reset in onDestroy is belt-and-suspenders, not a correctness fix.

The companion-object flag is slightly ugly, but it is not a race. No change required.

[MEDIUM] Notification icon — valid, will address

getApplicationInfo().icon at line 125 resolves to the launcher icon, which on API 26+ is typically rendered as a grey silhouette because it isn't alpha-only. Sibling VoipNotification.kt already uses a ic_notification drawable which is monochrome-safe. Will switch to the same pattern.

[MEDIUM] ACTION_STOP via startService causing a no-op instantiation — obsolete

This finding was written against the original context.startService(intent) path. Commit 4d9feb2 (landed in response to the CodeRabbit review, before the Opus review was dismissed) already switched stopService() to context.stopService(intent), which does not instantiate the service when it isn't running. The cited code no longer exists.

Production risk

Independent of the review: VoipCallService has no caller in this PR. It's scaffolding for PR-4 in the VoIP Android series. Merging today has zero runtime effect on production — the service is declared in the manifest but never started.

Plan

  1. Fix notification icon (cosmetic, one-line).
  2. Optionally drop callId from Log.d on lines 74/104 (minor privacy hygiene).
  3. Merge. Wiring lands in PR-4.

@diegolmello diegolmello merged commit 3653fa2 into feat.voip-lib-new Apr 22, 2026
5 of 10 checks passed
@diegolmello diegolmello deleted the feat/voip-android-call-service branch April 22, 2026 14:28
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