Skip to content

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

Merged
diegolmello merged 4 commits into
feat.voip-lib-newfrom
pr2-android-ddp-thread-safety
Apr 20, 2026
Merged

fix(voip): Android DDP thread safety and VoipPayload bundle parity#7168
diegolmello merged 4 commits into
feat.voip-lib-newfrom
pr2-android-ddp-thread-safety

Conversation

@diegolmello

@diegolmello diegolmello commented Apr 16, 2026

Copy link
Copy Markdown
Member

Proposed changes

Android VoIP DDP and cold-start payload hardening (split-plan PR 2):

  • DDPClient: use AtomicInteger for DDP message ids; mark isConnected and connectedCallback @Volatile for visibility across OkHttp and main-thread handlers.
  • Connect timeout: track a single Runnable for the connect timeout and remove only that callback instead of removeCallbacksAndMessages(null), so unrelated Handler work is not cleared. Cancel that timeout on connected, disconnect(), and WebSocket onFailure.
  • Stale-listener guard: onMessage, onFailure, and onClosed now bail early when the WebSocket parameter does not match the current active socket (webSocket !== this@DDPClient.webSocket). Prevents a closed socket's late OkHttp callbacks from hijacking a newly installed connectedCallback via a CAS win after reconnect.
  • disconnect() no longer resets connectResultDelivered: connect() already rearms the flag via resetConnectHandshakeState(); the extra reset in disconnect() narrowed the stale-listener guard window unnecessarily.
  • VoipPayload.toBundle(): include voipAcceptFailed so process-death / bundle restore matches fromBundle() and toWritableMap().
  • VoipModule: @Volatile on companion initialEventsData for cross-thread visibility.
  • Tests: Robolectric unit tests for connect-timeout vs connected ordering, voipAcceptFailed bundle round-trip, connection failure delivery (previously untested onFailure path), and stale-failure no-op regression lock.

Issue(s)

How to test or reproduce

From repo root after yarn:

cd android && ./gradlew :app:testOfficialDebugUnitTest --tests "chat.rocket.reactnative.voip.DDPClientTest" --tests "chat.rocket.reactnative.voip.VoipPayloadBundleTest"

Or run all VoIP JVM unit tests:

cd android && ./gradlew :app:testOfficialDebugUnitTest --tests "chat.rocket.reactnative.voip.*"

Screenshots

N/A (native concurrency and serialization only).

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

Draft PR: branch pr2-android-ddp-thread-safety. Base branch is feat.voip-lib-new.

Summary by CodeRabbit

  • Bug Fixes

    • Improve VoIP reliability: ensure handshake callback fires at most once, robustly handle timeouts and stale socket events, and make initial event handling and message sending concurrency-safe.
    • Include accept-failure flag in VoIP payload serialization for reliable process-recovery.
  • Tests

    • Add Android unit tests covering handshake timing, duplicate/late messages, timeout cancellation, stale-failure regressions, and payload bundle round-trips.
  • Chores

    • Enable Robolectric-based Android unit testing.

@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)

Walkthrough

Adds Robolectric test dependency; makes DDPClient ensure single connect outcome delivery with connect-timeout lifecycle, atomic send ids, stale-event guarding, and test hooks; converts VoipModule initial-event storage to an AtomicReference with claim semantics; persists voipAcceptFailed in VoipPayload Bundle; adds Robolectric tests for DDPClient and VoipPayload.

Changes

Cohort / File(s) Summary
Build / Test Dependency
android/app/build.gradle
Adds testImplementation 'org.robolectric:robolectric:4.14.1' for Robolectric unit tests.
DDPClient Connection Lifecycle & Test Hooks
android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt
Guarantees single-delivery handshake using an AtomicBoolean, manages connect-timeout scheduling/cancellation (including on disconnect), guards against stale WebSocket events by validating webSocket identity, switches send id to AtomicInteger, and exposes internal test hooks for timeouts, raw message injection, simulated failures, and active WebSocket control.
VoIP State Management (atomic claim)
android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt
Replaces nullable shared initialEventsData with AtomicReference<VoipPayload?>; updates store/clear/get flows to use set/compareAndSet with retry-to-claim semantics to avoid racey clears.
VoIP Payload Serialization
android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt
Adds voipAcceptFailed to toBundle() so the flag is written into the Android Bundle and round-trips via fromBundle().
DDPClient Tests (Robolectric)
android/app/src/test/java/chat/rocket/reactnative/voip/DDPClientTest.kt
New Robolectric tests using Looper time control and DDPClient test hooks to verify single callback delivery across connected, timeout, duplicate, stale-failure, reconnect, and disconnect scenarios; includes a StubWebSocket helper.
VoipPayload Bundle Tests (Robolectric)
android/app/src/test/java/chat/rocket/reactnative/voip/VoipPayloadBundleTest.kt
New Robolectric tests asserting voipAcceptFailed presence in Bundle and correct round-trip via VoipPayload.fromBundle().

Sequence Diagram(s)

sequenceDiagram
    actor App as App/Client
    participant DDP as DDPClient
    participant WS as WebSocket
    participant Handler as MainHandler
    participant CB as ConnectCallback

    App->>DDP: waitForConnected(callback)
    DDP->>Handler: post connect-timeout runnable
    DDP->>WS: open/connect()

    alt WS receives "connected"
        WS->>DDP: onMessage({"msg":"connected"})
        DDP->>DDP: tryDeliverConnectOutcome(true)
        DDP->>Handler: cancel timeout runnable
        DDP->>CB: invoke(callback, true)
    else WS failure
        WS->>DDP: onFailure(...)
        DDP->>DDP: validate webSocket identity
        DDP->>DDP: tryDeliverConnectOutcome(false)
        DDP->>Handler: cancel timeout runnable
        DDP->>CB: invoke(callback, false)
    else Timeout fires
        Handler->>DDP: timeout runnable runs
        DDP->>DDP: tryDeliverConnectOutcome(false)
        DDP->>CB: invoke(callback, false)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: fixing Android DDP thread safety issues and improving VoipPayload bundle parity across serialization methods.

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

@diegolmello
diegolmello changed the base branch from develop to feat.voip-lib-new April 16, 2026 20:41
@diegolmello
diegolmello had a problem deploying to experimental_android_build April 16, 2026 20:45 — with GitHub Actions Error
@diegolmello
diegolmello had a problem deploying to official_android_build April 16, 2026 20:45 — with GitHub Actions Error
@diegolmello
diegolmello had a problem deploying to experimental_ios_build April 16, 2026 20:45 — with GitHub Actions Error
@diegolmello
diegolmello marked this pull request as ready for review April 17, 2026 21:42

@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/DDPClient.kt`:
- Line 45: connectTimeoutRunnable is concurrently accessed from OkHttp threads
and the main looper, risking stale reads; modify waitForConnected(),
cancelConnectTimeout(), and the timeout Runnable so they use the same
cross-thread protection as connectedCallback — either marshal all reads/writes
to the main Handler (post/execute mutations on the main looper) or guard
connectTimeoutRunnable and connectedCallback with the same lock (synchronized
block or ReentrantLock) so cancellation cannot be missed and the timeout
Runnable cannot fire after cancelConnectTimeout() returns.
- Around line 74-81: The connect callback is invoked from multiple paths
(onFailure, handleMessage("connected") via connectedCallback, and the
waitForConnected timeout), causing duplicate success/failure reports; introduce
an AtomicBoolean (e.g., connectionResultDelivered) and use compareAndSet(false,
true) at the start of each place that posts the final result (onFailure, the
code that invokes connectedCallback/connectedCallback?.invoke(true) in
handleMessage, and the timeout runnable in waitForConnected) so only the first
outcome is delivered to the caller; ensure each path still cancels the timeout
(cancelConnectTimeout) and clears connectedCallback/connectedResult references
as before when compareAndSet succeeds, and skip invoking
callback/connectedCallback when compareAndSet returns false.

In `@android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt`:
- Around line 24-25: Replace the Volatile backing field initialEventsData with
an AtomicReference<VoipPayload?> and update callers (storeInitialEvents(),
storeAcceptFailureForJs()) to use AtomicReference.set/get; in getInitialEvents()
read the current payload via initialEventsData.get(), check expiration, and
clear only the exact consumed payload using
initialEventsData.compareAndSet(data, null) so a concurrent writer that replaced
the payload is not lost; ensure you perform the CAS both on expired and
on-successful-consume paths so only the payload you observed is cleared.
🪄 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: 0215890c-29ca-43f0-a9c2-004b6c379df3

📥 Commits

Reviewing files that changed from the base of the PR and between 1e2e6e8 and 1756096.

📒 Files selected for processing (6)
  • android/app/build.gradle
  • android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt
  • android/app/src/test/java/chat/rocket/reactnative/voip/DDPClientTest.kt
  • android/app/src/test/java/chat/rocket/reactnative/voip/VoipPayloadBundleTest.kt
📜 Review details
🧰 Additional context used
🧠 Learnings (3)
📓 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/VoipModule.kt
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt
📚 Learning: 2026-03-31T11:59:31.061Z
Learnt from: Rohit3523
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6875
File: android/build.gradle:3-8
Timestamp: 2026-03-31T11:59:31.061Z
Learning: In the RocketChat/Rocket.Chat.ReactNative repository, the React Native upgrade helper (https://react-native-community.github.io/upgrade-helper/?from=0.79.4&to=0.81.5) recommends kotlinVersion = "2.1.20", compileSdkVersion = 36, targetSdkVersion = 36, and buildToolsVersion = "36.0.0" in android/build.gradle for the RN 0.79.4 → 0.81.5 upgrade. These are the sanctioned values for this upgrade path and should not be flagged as compatibility concerns.

Applied to files:

  • android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt
🔇 Additional comments (5)
android/app/build.gradle (1)

158-158: Test dependency is correctly scoped.

Robolectric is added as testImplementation, so the new JVM tests are enabled without adding it to the production APK.

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

75-75: Bundle parity looks good.

toBundle() now preserves voipAcceptFailed, matching the existing fromBundle() path and preventing cold-start accept-failure state loss.

android/app/src/test/java/chat/rocket/reactnative/voip/VoipPayloadBundleTest.kt (1)

15-60: Good regression coverage for bundle round-trip.

The tests cover both boolean values, and the false case would fail if the key were omitted because it reads with default true.

android/app/src/test/java/chat/rocket/reactnative/voip/DDPClientTest.kt (1)

19-60: Timeout behavior coverage looks focused.

The tests validate success, timeout failure, and that canceling the connect timeout no longer removes unrelated main-handler work.

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

14-14: Atomic message IDs look good.

Switching sendCounter to AtomicInteger makes concurrent DDP message ID generation safe without changing the generated ID format.

Also applies to: 32-32, 158-163

Comment thread android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt
Comment thread android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt
Comment thread android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt Outdated
@diegolmello
diegolmello force-pushed the pr2-android-ddp-thread-safety branch from 1756096 to fb4de9e Compare April 20, 2026 13:34
@diegolmello
diegolmello had a problem deploying to experimental_ios_build April 20, 2026 13:38 — with GitHub Actions Error
@diegolmello
diegolmello had a problem deploying to official_android_build April 20, 2026 13:38 — with GitHub Actions Error
@diegolmello
diegolmello had a problem deploying to experimental_android_build April 20, 2026 13:38 — with GitHub Actions Error

@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)
android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt (1)

349-357: Prefer @VisibleForTesting for test-only entry points.

These are package-visible via internal but aren't conceptually part of the production API surface. Annotating with androidx.annotation.VisibleForTesting documents intent and lets lint warn on accidental production use.

♻️ Suggested annotation
+import androidx.annotation.VisibleForTesting
@@
-    internal fun testStartConnectTimeout(timeoutMs: Long, callback: (Boolean) -> Unit) {
+    `@VisibleForTesting`
+    internal fun testStartConnectTimeout(timeoutMs: Long, callback: (Boolean) -> Unit) {
         resetConnectHandshakeState()
         waitForConnected(timeoutMs, callback)
     }
 
-    internal fun testDeliverRawMessage(text: String) {
+    `@VisibleForTesting`
+    internal fun testDeliverRawMessage(text: String) {
         handleMessage(text)
     }
🤖 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/DDPClient.kt` around
lines 349 - 357, Annotate the test-only entry points with
androidx.annotation.VisibleForTesting to document intent and enable lint checks:
add `@VisibleForTesting` (importing androidx.annotation.VisibleForTesting) above
the internal functions testStartConnectTimeout and testDeliverRawMessage in
DDPClient (DDPClient.kt) so they remain package-visible but are marked
explicitly as testing-only API.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt`:
- Around line 349-357: Annotate the test-only entry points with
androidx.annotation.VisibleForTesting to document intent and enable lint checks:
add `@VisibleForTesting` (importing androidx.annotation.VisibleForTesting) above
the internal functions testStartConnectTimeout and testDeliverRawMessage in
DDPClient (DDPClient.kt) so they remain package-visible but are marked
explicitly as testing-only API.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c11971fd-36af-451b-ac8d-cd9a19a55119

📥 Commits

Reviewing files that changed from the base of the PR and between 1756096 and fb4de9e.

📒 Files selected for processing (6)
  • android/app/build.gradle
  • android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt
  • android/app/src/test/java/chat/rocket/reactnative/voip/DDPClientTest.kt
  • android/app/src/test/java/chat/rocket/reactnative/voip/VoipPayloadBundleTest.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt
  • android/app/src/test/java/chat/rocket/reactnative/voip/VoipPayloadBundleTest.kt
  • android/app/src/test/java/chat/rocket/reactnative/voip/DDPClientTest.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). (2)
  • GitHub Check: ESLint and Test / run-eslint-and-test
  • GitHub Check: format
🔇 Additional comments (4)
android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt (1)

106-123: LGTM — atomic consume-and-clear correctly addresses the prior race.

The retry loop now safely handles concurrent writers: each iteration re-reads via get(), and both the expired-discard and successful-consume paths use compareAndSet(data, null) so only the exact observed payload is cleared. If a concurrent storeInitialEvents/storeAcceptFailureForJs replaces the reference between get() and CAS, the CAS fails and the loop re-reads the new payload instead of dropping it — directly resolving the earlier feedback.

One minor note (non-blocking): on the consume path, toWritableMap() is built before the CAS, so if CAS fails the WritableMap is discarded and rebuilt on the next iteration. In practice writers are rare (notification/REST/timeout handlers) and payloads are small, so the wasted work is negligible.

android/app/build.gradle (1)

157-158: LGTM — Robolectric added as testImplementation only.

Scoped correctly so it won't affect release APK size, and pairs fine with the existing JUnit 4.13.2. This is the minimum needed to run the new DDPClientTest/VoipPayloadBundleTest suites under the JVM unit test task.

android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt (2)

33-49: Thread-safety hardening looks correct.

AtomicInteger for id generation, @Volatile on isConnected, connectedCallback, and connectTimeoutRunnable, and the AtomicBoolean gating for one-shot callback delivery together address the previously flagged races (OkHttp thread vs. main looper). The cross-thread visibility story for these fields is now consistent.


53-93: The per-call DDPClient architecture prevents the described race condition from occurring in practice.

The codebase creates a fresh DDPClient instance for each callId (line 447 of VoipNotification.kt), calls connect() exactly once per instance (line 499), and manages cleanup via VoipPerCallDdpRegistry. When a new client replaces an old one for the same callId, the registry invokes disconnect() on the old client (via the releaseClient lambda at line 79), which comprehensively tears down the prior state:

  • Closes the WebSocket
  • Nulls connectedCallback
  • Cancels the timeout via cancelConnectTimeout()
  • Clears all pending callbacks

No code path calls connect() multiple times on the same instance, and no retry logic exists. The "cross-connect race" scenario outlined in the review (where onFailure from an old socket fires after connect() is called again on the same instance) cannot occur given the current single-use-per-call architecture. The suggested hardening would be defensive but is not required by any observable usage pattern.

- DDPClient: AtomicInteger for DDP ids; @volatile isConnected/connectedCallback
- Track connect-timeout Runnable; cancel only that runnable (not all Handler work)
- Clear connect timeout on WebSocket failure to avoid duplicate callbacks
- VoipPayload.toBundle: persist voipAcceptFailed for process-death restore
- VoipModule: @volatile initialEventsData for cross-thread visibility
- Add Robolectric unit tests for timeout/callback races and bundle round-trip

Made-with: Cursor
- DDPClient: @volatile connectTimeoutRunnable; AtomicBoolean gates single connect outcome
- VoipModule: AtomicReference + CAS for initialEventsData consume/clear
- DDPClientTest: duplicate connected delivers callback once

Made-with: Cursor
@diegolmello
diegolmello force-pushed the pr2-android-ddp-thread-safety branch from fb4de9e to 7c63d4c Compare April 20, 2026 15:03
@diegolmello
diegolmello had a problem deploying to experimental_ios_build April 20, 2026 15:06 — with GitHub Actions Error
@diegolmello
diegolmello had a problem deploying to official_android_build April 20, 2026 15:06 — with GitHub Actions Error
@diegolmello
diegolmello had a problem deploying to experimental_android_build April 20, 2026 15:06 — with GitHub Actions Error

@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)
android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt (1)

53-93: ⚠️ Potential issue | 🟠 Major

Close any prior webSocket before opening a new one.

connect() reassigns webSocket without closing/nulling the previous instance. If a caller invokes connect() twice (or re-connects without disconnect()), the old WebSocketListener stays registered and can still fire onOpen/onFailure/onMessage. Because those callbacks route through shared fields (connectedCallback, connectResultDelivered, isConnected) rather than closure state, a stale listener can overwrite/deliver into the new handshake's flow (e.g., an old onFailure posting tryDeliverConnectOutcome(false) after the new waitForConnected has already installed connectedCallback = newCb).

🔒 Suggested hardening
 fun connect(host: String, callback: (Boolean) -> Unit) {
     resetConnectHandshakeState()
+    // Ensure any prior socket/listener is torn down so stale events cannot
+    // deliver into the new handshake via shared state.
+    webSocket?.close(1000, null)
+    webSocket = null
+    connectedCallback = null
+    cancelConnectTimeout()
 
     val wsUrl = buildWebSocketURL(host)
🤖 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/DDPClient.kt` around
lines 53 - 93, connect() currently reassigns the webSocket field without closing
the prior socket, allowing stale listeners to interfere with a new handshake;
before creating a new WebSocket in connect(), if webSocket is non-null call
webSocket.close(1000, "reconnect") (or webSocket.cancel() if you prefer
immediate termination) and null out the field to ensure the old listener won't
race into the new connect flow; update the connect method to perform this
close/null sequence right before building/assigning the new webSocket
(referencing the webSocket field and the connect() function and the
client.newWebSocket call).
🤖 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/DDPClient.kt`:
- Around line 150-161: The disconnect() implementation should stop resetting the
connect-handshake atomic so late events from the old socket cannot hijack a
newly installed callback: remove the line that clears or sets
connectResultDelivered (the atomic used by tryDeliverConnectOutcome) from
disconnect(), and rely on resetConnectHandshakeState() which connect() already
calls to rearm connectResultDelivered; keep cancelling timeouts, clearing
pendingCallbacks, clearing connectedCallback, nulling onCollectionMessage and
closing webSocket as-is so that onFailure/onMessage from the old socket will
fail the CAS in tryDeliverConnectOutcome and not invoke a new connectedCallback.

---

Outside diff comments:
In `@android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt`:
- Around line 53-93: connect() currently reassigns the webSocket field without
closing the prior socket, allowing stale listeners to interfere with a new
handshake; before creating a new WebSocket in connect(), if webSocket is
non-null call webSocket.close(1000, "reconnect") (or webSocket.cancel() if you
prefer immediate termination) and null out the field to ensure the old listener
won't race into the new connect flow; update the connect method to perform this
close/null sequence right before building/assigning the new webSocket
(referencing the webSocket field and the connect() function and the
client.newWebSocket call).
🪄 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: fbcfd3fe-11f1-4cc9-93e4-aab7435c989b

📥 Commits

Reviewing files that changed from the base of the PR and between fb4de9e and 7c63d4c.

📒 Files selected for processing (6)
  • android/app/build.gradle
  • android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt
  • android/app/src/test/java/chat/rocket/reactnative/voip/DDPClientTest.kt
  • android/app/src/test/java/chat/rocket/reactnative/voip/VoipPayloadBundleTest.kt
✅ Files skipped from review due to trivial changes (2)
  • android/app/build.gradle
  • android/app/src/test/java/chat/rocket/reactnative/voip/VoipPayloadBundleTest.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt
  • android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt
  • android/app/src/test/java/chat/rocket/reactnative/voip/DDPClientTest.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). (2)
  • GitHub Check: ESLint and Test / run-eslint-and-test
  • GitHub Check: format
🧰 Additional context used
🧠 Learnings (1)
📓 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

Comment thread android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt
…nect()

- Add stale-listener guard (webSocket !== this@DDPClient.webSocket) to
  onMessage, onFailure, and onClosed; a socket closed via disconnect()
  can still fire OkHttp callbacks after webSocket is nulled or replaced,
  and without the guard those callbacks could win the connectResultDelivered
  CAS and invoke a newly installed connectedCallback with a spurious false.
- Remove connectResultDelivered.set(false) from disconnect(); connect()
  already rearms the flag via resetConnectHandshakeState(), so the reset
  in disconnect() only narrows the guard window unnecessarily.
- Add testDeliverConnectFailure() internal hook and two new tests:
  "connection failure delivers false exactly once" (covers previously
  untested onFailure path) and "stale failure after completed connect and
  disconnect is a no-op" (regression lock for the CAS hijack).

Addresses CodeRabbit review comment on PR #7168 (discussion_r3111670137).
@diegolmello
diegolmello had a problem deploying to experimental_ios_build April 20, 2026 16:33 — with GitHub Actions Error
@diegolmello
diegolmello had a problem deploying to experimental_android_build April 20, 2026 16:33 — with GitHub Actions Error

@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

🧹 Nitpick comments (1)
android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt (1)

352-363: Test-only hooks on the production class — acceptable, but consider tightening.

Exposing testStartConnectTimeout, testDeliverRawMessage, and testDeliverConnectFailure as internal is fine for unit testing, but they widen the production surface area (any code in the same module can call them). If you want to keep the handshake helpers testable without this, a common alternative is @VisibleForTesting(otherwise = PRIVATE) from androidx.annotation paired with Lint enforcement, or route tests through a narrow package-private seam. Not a blocker.

🤖 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/DDPClient.kt` around
lines 352 - 363, The three test hooks testStartConnectTimeout,
testDeliverRawMessage, and testDeliverConnectFailure are currently declared
internal which broadens the production API; change their visibility to private
and annotate them with `@VisibleForTesting`(otherwise = PRIVATE) from
androidx.annotation (or move them behind a narrow package-private test seam) so
tests can access them while preventing other production-module code from calling
them; update imports to include androidx.annotation.VisibleForTesting and keep
the existing implementations of resetConnectHandshakeState, waitForConnected,
handleMessage, and tryDeliverConnectOutcome unchanged.
🤖 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/DDPClient.kt`:
- Around line 81-88: The onFailure handler posts a runnable that calls
tryDeliverConnectOutcome(false) but only checks webSocket identity before
posting, allowing a stale failure to hijack a new connect; fix by re-checking
the failing WebSocket inside the posted runnable (or capture the failing
WebSocket and pass it into tryDeliverConnectOutcome so that
tryDeliverConnectOutcome verifies it against this@DDPClient.webSocket) before
performing the CAS/delivery, similar to the pre-post guard; update onFailure
(and any other WebSocket-thread callbacks that post to mainHandler) to
capture/compare the event's webSocket and no-op if it no longer matches the
current this@DDPClient.webSocket, ensuring
disconnect()/connect()/resetConnectHandshakeState() can't be hijacked.

In `@android/app/src/test/java/chat/rocket/reactnative/voip/DDPClientTest.kt`:
- Around line 92-117: The test only verifies the CAS gate because
testDeliverConnectFailure() posts tryDeliverConnectOutcome(false) directly, so
add a scenario that simulates the real hijack window where disconnect() is
followed by a quick connect() before a stale WebSocket onFailure() arrives:
after client.disconnect(), call client.testStartConnectTimeout(...) to arm a new
callback (i.e., simulate a new connect()), then invoke
client.testDeliverConnectFailure() (so the stale onFailure is delivered while a
new handshake is armed) and assert the new callback does not receive false; this
also verifies resetConnectHandshakeState()/disconnect() doesn’t re-arm the
handshake state incorrectly and coordinates with DDPClient.onFailure handling.

---

Nitpick comments:
In `@android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt`:
- Around line 352-363: The three test hooks testStartConnectTimeout,
testDeliverRawMessage, and testDeliverConnectFailure are currently declared
internal which broadens the production API; change their visibility to private
and annotate them with `@VisibleForTesting`(otherwise = PRIVATE) from
androidx.annotation (or move them behind a narrow package-private test seam) so
tests can access them while preventing other production-module code from calling
them; update imports to include androidx.annotation.VisibleForTesting and keep
the existing implementations of resetConnectHandshakeState, waitForConnected,
handleMessage, and tryDeliverConnectOutcome unchanged.
🪄 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: 052737ab-77bf-410b-a8cb-5775b3642f7d

📥 Commits

Reviewing files that changed from the base of the PR and between 7c63d4c and 3e6b128.

📒 Files selected for processing (2)
  • android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt
  • android/app/src/test/java/chat/rocket/reactnative/voip/DDPClientTest.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). (2)
  • GitHub Check: ESLint and Test / run-eslint-and-test
  • GitHub Check: format
🧰 Additional context used
🧠 Learnings (1)
📓 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
🔇 Additional comments (1)
android/app/src/test/java/chat/rocket/reactnative/voip/DDPClientTest.kt (1)

19-90: Handshake behavior coverage looks solid.

Good coverage for the refactor: at-most-once delivery on connected before timeout, cancelConnectTimeout no longer nuking unrelated main-handler runnables (regression for the prior removeCallbacksAndMessages(null)), timeout-without-connected single false, duplicate connected idempotency, and duplicate-failure dedup. Robolectric idle() / idleFor() usage correctly exercises mainHandler.post / postDelayed paths.

Comment thread android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt
…check

CodeRabbit flagged that the outer stale-listener guard in onFailure() runs
on the OkHttp thread before mainHandler.post. If disconnect() and a new
connect() interleave on the main thread between the outer guard and the
runnable's execution, the new connect() calls resetConnectHandshakeState()
(delivered=false) and installs a fresh connectedCallback via waitForConnected.
The stale runnable then wins the CAS and hijacks the new caller's callback
with false.

Fix:
- Re-check webSocket !== this@DDPClient.webSocket INSIDE the mainHandler.post
  block in onFailure so stale failures remain no-ops even across a quick
  disconnect/reconnect.
- Extend testDeliverConnectFailure to optionally take a fromWebSocket so the
  test harness can simulate the real OkHttp-thread → main-thread post flow
  including the identity guard.
- Add testSetActiveWebSocket() helper to simulate the current-active-socket
  transition that disconnect()+connect() would produce in production.
- Add StubWebSocket and a new regression test
  ("stale failure during reconnect window does not hijack new callback")
  that exercises the full hijack vector end-to-end.
- Rename existing "stale failure after completed connect and disconnect is
  a no-op" test to clarify it covers only the CAS-gate case, not the
  reconnect-window case.

Addresses CodeRabbit reviews discussion_r3112263091 (onFailure guard) and
related regression-test weakness comment on the new DDPClientTest.
@diegolmello
diegolmello had a problem deploying to official_android_build April 20, 2026 18:32 — with GitHub Actions Failure
@diegolmello
diegolmello had a problem deploying to experimental_ios_build April 20, 2026 18:32 — with GitHub Actions Failure
@diegolmello
diegolmello had a problem deploying to experimental_android_build April 20, 2026 18:32 — with GitHub Actions Failure

@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/DDPClient.kt (1)

63-74: ⚠️ Potential issue | 🟠 Major

Add the stale-socket guard to onOpen too.

onOpen can still run from an old listener after disconnect()/quick reconnect. Without the same identity check, it can call waitForConnected() and overwrite the active connectedCallback/timeout with stale state.

Suggested fix
             override fun onOpen(webSocket: WebSocket, response: Response) {
+                if (webSocket !== this@DDPClient.webSocket) return
                 Log.d(TAG, "WebSocket opened")
                 val connectMsg = JSONObject().apply {
                     put("msg", "connect")
🤖 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/DDPClient.kt` around
lines 63 - 74, onOpen currently proceeds unconditionally and can act on a stale
WebSocket; add the same stale-socket guard used elsewhere to verify the incoming
WebSocket matches the active instance (e.g., compare the webSocket parameter to
the class's current socket/ws field) and bail out if they differ, so onOpen does
not call waitForConnected or overwrite the active connected callback/timeout for
an old listener; update the onOpen handler in DDPClient.kt accordingly (check
socket identity before building/sending connectMsg and before invoking
waitForConnected).
🤖 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/DDPClient.kt`:
- Around line 63-74: onOpen currently proceeds unconditionally and can act on a
stale WebSocket; add the same stale-socket guard used elsewhere to verify the
incoming WebSocket matches the active instance (e.g., compare the webSocket
parameter to the class's current socket/ws field) and bail out if they differ,
so onOpen does not call waitForConnected or overwrite the active connected
callback/timeout for an old listener; update the onOpen handler in DDPClient.kt
accordingly (check socket identity before building/sending connectMsg and before
invoking waitForConnected).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a977ad0c-95e4-4285-96cf-01fd148adc1e

📥 Commits

Reviewing files that changed from the base of the PR and between 3e6b128 and 54a7cea.

📒 Files selected for processing (2)
  • android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt
  • android/app/src/test/java/chat/rocket/reactnative/voip/DDPClientTest.kt
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📓 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
🔇 Additional comments (12)
android/app/src/test/java/chat/rocket/reactnative/voip/DDPClientTest.kt (5)

18-25: Stub is minimal and fit for these listener-identity tests.

The fake WebSocket keeps the reconnect-window tests focused on object identity without pulling in OkHttp internals.


31-72: Good coverage for timeout ordering and targeted timeout cancellation.

These tests lock down both “connected wins before timeout” and “cancel only the connect timeout, not unrelated main-handler work.”


74-102: Good single-delivery coverage for duplicate success/failure paths.

This directly validates the AtomicBoolean handshake gate from both connected-message and failure-delivery directions.


104-127: Good regression lock for late failure after disconnect.

This confirms disconnect() no longer re-arms connectResultDelivered, so a late failure cannot emit a spurious false when no reconnect is active.


129-169: Reconnect-window hijack regression is covered.

This addresses the prior stale-failure gap by queuing a failure for the old socket, installing a new socket/callback, and asserting the stale runnable cannot claim the new callback.

android/app/src/main/java/chat/rocket/reactnative/voip/DDPClient.kt (7)

33-49: Thread-safety hardening looks good.

AtomicInteger, volatile connection state, volatile callback storage, and the handshake AtomicBoolean are appropriate for the OkHttp/main-thread split here.


76-99: Stale listener checks and the inner failure re-check look solid.

The second identity check inside the posted onFailure runnable closes the reconnect-window race where an old failure could otherwise claim a freshly installed callback.


158-168: disconnect() now preserves the handshake CAS state correctly.

Cancelling the timeout and clearing callbacks without resetting connectResultDelivered prevents late old-socket events from becoming deliverable before the next explicit connect re-arms state.


170-175: Atomic DDP ids are a good fix.

Using incrementAndGet() removes the cross-thread race around message id generation.


239-276: Centralized connect outcome delivery is much safer.

The CAS gate plus targeted timeout removal keeps success, failure, and timeout paths single-delivery and avoids wiping unrelated main-handler callbacks.


286-290: Connected-message handling now uses the same delivery gate.

Routing "connected" through tryDeliverConnectOutcome(true) keeps it consistent with failure and timeout paths.


357-375: Test hooks are appropriately narrow for the new race tests.

The hooks expose only handshake timing, raw message delivery, failure simulation, and active-socket identity needed by the Robolectric coverage.

@diegolmello
diegolmello merged commit a10bf80 into feat.voip-lib-new Apr 20, 2026
6 of 11 checks passed
@diegolmello
diegolmello deleted the pr2-android-ddp-thread-safety branch April 20, 2026 18:48
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