Skip to content

fix(android): the foreground service keeps an app's channel and starts without the module - #904

Merged
sboily merged 2 commits into
masterfrom
fix/android-foreground-service-channel
Oct 17, 2026
Merged

sboily merged 2 commits into
masterfrom
fix/android-foreground-service-channel

Conversation

@sboily

@sboily sboily commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Two fixes in VoiceConnectionService.startForegroundService(). Both come from the patch Luge Mobile carries on 4.4.0.

An app's own channel lost its importance

foregroundService.channelId can name a channel the app already created, such as the one its incoming calls ring in. startForegroundService() created it again with IMPORTANCE_NONE, and Android lets an app lower the importance of its own channel as long as the user hasn't changed it. On a Galaxy S24 with Android 16 it went from high (4) to low (2), and the app's notifications in that channel stopped alerting.

An existing channel now keeps its importance and sound, and still takes channelName, so a renamed or translated name is applied as before. CallKeep still creates the channel when it doesn't exist. docs/setup.md says so for channelId.

A call in a process without the module

The service read its settings with getForegroundSettings(null), which returns them only once the module, or another caller with a context, has loaded them. In a process where the module doesn't exist:

  • a call placed from the Phone app while the app isn't running found no settings, so makeOutgoingCall started no foreground service (no crash);
  • an incoming call loads the stored settings in onCreateIncomingConnection (getSettings(this)), then startForegroundService() read RNCallKeepModule.instance without a check: NullPointerException. That happens for a call added from native code (or with setupNative, feat: show an incoming call from native code, and observe events natively on iOS #906).

The service now reads its settings with its own context, so both calls get their foreground service. The null check on the module covers both; without the module, the notification opens no activity.

Tests

tests/android/CallKeepForegroundServiceTest.kt, with a small options helper that the existing test now uses too. Run in the example app (React Native 0.87.1) on a Galaxy S24, Android 16:

master this branch
theForegroundServiceKeepsTheAppsOwnChannel (importance kept, name from channelName) fails: expected:<4> but was:<2> OK
theForegroundServiceStartsWithoutTheModule (stored settings, no module, incoming call added through Telecom) process crashes: NullPointerException on getCurrentReactActivity() OK
  • Channel test: it uses a new channel id on every run, because Android restores a deleted channel's settings when its id is created again.
  • Phone app path: the outgoing call from the Phone app has no test. makeOutgoingCall wakes the app up, which would start React Native inside the test process.
  • Lint and checks: ./gradlew :react-native-callkeep:lintDebug reports no errors, and npm run check passes.
  • Existing test: endingTheSecondCallKeepsTheForegroundServiceOfTheFirst fails on this device ("second call not created"), on master as well as on this branch. I haven't looked into it here.
  • Host setup: the example app needs RECORD_AUDIO granted for these fixtures. Its manifest declares phoneCall|microphone, and without the permission Android 14+ refuses the service: SecurityException: Starting FGS with type microphone ... requires permissions. That is the case feat(android): foregroundServiceType and endCallsOnHostDestroy options #905's foregroundServiceType addresses.

🤖 Generated with Claude Code

…s without the module

startForegroundService created the channel named by
foregroundService.channelId every time, with IMPORTANCE_NONE. When that
channel already exists because the app created it (an app can pass its
own incoming-call channel), Android lets the app lower its importance: on
a Galaxy S24 with Android 16 it went from high to low, and the app's
notifications in it stopped alerting. An existing channel is now left as
it is.

It also read RNCallKeepModule.instance without a check. Telecom can
create a connection in a process where the module was never created, for
instance a call placed from the Phone app while the app isn't running
(makeOutgoingCall starts the service), and the app crashed with a
NullPointerException. Without the module, the notification opens no
activity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…channel takes its name, per review

The foreground service read its settings with getForegroundSettings(null),
which only works once the module or another caller has loaded them. In
a process without the module, a call placed from the Phone app found no
settings and started no foreground service (no crash, as the first
commit wrongly said), while an incoming call, whose
onCreateIncomingConnection loads them with getSettings(this), went on
to the NullPointerException. The service now reads them with its own
context, so both get their foreground service, and the null check on
the module covers both.

An existing channel still keeps its importance and sound, but takes
channelName again, as creating it did: a renamed or translated channel
name was no longer applied.

The tests follow: the channel test checks the name, and the test without
the module also clears the loaded settings, as in a new process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sboily
sboily merged commit 2ec7933 into master Oct 17, 2026
4 checks passed
@sboily
sboily deleted the fix/android-foreground-service-channel branch October 17, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant