Skip to content

fix(android): calls outlive the activity, as in other calling apps - #908

Merged
sboily merged 1 commit into
masterfrom
fix/android-calls-outlive-activity
Oct 17, 2026
Merged

sboily merged 1 commit into
masterfrom
fix/android-calls-outlive-activity

Conversation

@sboily

@sboily sboily commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

The problem

onHostDestroy, added in #724 to "stop all calls and processes when app is killed", ends every call and kills the process (killProcess) whenever the React activity is destroyed. That is not the app being killed. On Android 10 and Android 16 emulators, with the example app and an answered call:

During the call master this PR
Font size changed (any configuration change the activity doesn't handle: language, display size...) call ended, app closed call goes on
Back (Android 11 and earlier destroy the root activity) call ended, app closed call goes on
App swiped away from Recents call ended, app closed call goes on
App process crashes Telecom ends the call Telecom ends the call (unchanged)
React Native reloads its JavaScript (debug build) call stays active in Telecom, no JavaScript handles it call ended

In the first three cases the process and React Native's runtime keep running (React reuses its instance and only mounts the screen again). Telecom keeps the process running during a call, with or without the foreground service: swiping the app away without foregroundService also leaves the process and the call (checked on both versions). Service.onTaskRemoved is never called on the ConnectionService when the app is swiped away, so it's no signal either.

The change

A call lasts as long as something can handle it, as in WhatsApp or Teams:

The example keeps its call state and CallKeep's listeners outside the screen, registered once from index.js: before, its calls lived in the component's state, so a recreated activity showed no call even though it went on. setup() still runs from the screen (it can ask for permissions on Android), once per mode, and the events replayed from before it (didLoadWithEvents) wait for it: a call started from Recents on an iOS cold start needs it. docs/setup.md has a Calls and the app's lifecycle (Android) section; the changelog lists it under Changed.

Tests

  • tests/android/CallKeepCallLifetimeOptionsTest.kt: theCallOutlivesTheActivity (master: the process is killed, reported as a crash), theCallEndsWhenReactNativeStops (master: the call outlives React Native), nativeCallsOutliveReactNative; with the type test, OK (4 tests). The other eight fixtures, including feat: show an incoming call from native code, and observe events natively on iOS #906's native setup, pass on Android 10.
  • The table above: the example app, release builds (debug for the reload, with its own Metro), on Android 10 and 16 emulators. With this PR, after each of the first three events the reopened app shows Alice (example) · active, and hanging up from it leaves no call in dumpsys telecom. The mode switch still runs setup() again.
  • The example passes tsc and ESLint. Its JavaScript changed on iOS too (listeners at startup, the replay waiting for setup()): on an iPhone 15 Pro Max with iOS 27, an incoming call works, and calling back from the Phone app's Recents with the app not running opens it and starts the call.

onHostDestroy, added in #724 to "stop all calls and processes when the
app is killed", ended every call and killed the process whenever the
React activity was destroyed. That is not the app being killed: a
configuration change the activity doesn't handle (font size, language),
Back on Android 11 and earlier, or swiping the app away from Recents
destroy the activity while the process and React Native's runtime keep
running. On Android 10 and 16 emulators, each of these ended an active
call and closed the app.

A call now lasts as long as something can handle it:
- the activity is destroyed: the call goes on, as in WhatsApp or Teams.
  Telecom keeps the process running during a call, with or without the
  foreground service (checked on both versions);
- the process dies: Telecom ends its calls itself (checked: a crash
  leaves no call);
- React Native's instance stops while the process runs (a reload, an
  over-the-air update): the module's invalidate() ends the calls on the
  main thread. On master a reload left the call active in Telecom with
  no JavaScript to handle it. endCallsWhenReactNativeStops: false keeps
  them for apps whose native code owns the calls. It replaces the
  unreleased endCallsOnHostDestroy option.

The example keeps its call state and CallKeep's listeners outside the
screen, registered once from index.js, so a new activity shows the call
that goes on; setup() still runs from the screen, and the events replayed
from before it wait for it. docs/setup.md explains the lifecycle.
@sboily
sboily merged commit fb18e7e into master Oct 17, 2026
4 checks passed
@sboily
sboily deleted the fix/android-calls-outlive-activity branch October 17, 2026 22:55
@sboily sboily self-assigned this Oct 17, 2026
@sboily sboily added the bug Something isn't working label Oct 17, 2026
@sboily sboily mentioned this pull request Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant