Repository navigation
feat: show an incoming call from native code, and observe events natively on iOS - #906
Merged
Merged
Conversation
…vely on iOS A call push can wake an app whose JavaScript isn't running, or isn't ready, yet the call has to ring at once. On Android, the setup and displayIncomingCall paths needed the React Native module; on iOS, native code had no way to hear the call's actions. Android (self-managed, Android 8 and later): - RNCallKeepModule.setupNative(context, options) stores the settings and registers the phone account as setup() does, without creating a module or a React context. The helpers it needs (initializeTelecomManager, registerPhoneAccount, storeSettings, getApplicationName, isSelfManaged) become static and take the context as an argument. - RNCallKeepModule.displayIncomingCall(context, ...) shows the call with that phone account. The app handles the call from CallKeep's local broadcasts, as before. - VoiceConnectionService.nativeFocusListener and hasConnectionServiceFocus() expose Telecom's call focus (Android 9 and later), so native code can start the call's audio before React Native runs and release it when the focus is lost. iOS: sendEventWithNameWrapper also posts each event to NotificationCenter under its native name, with the body as userInfo. The JavaScript events are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Telecom calls no onConnectionServiceFocusLost when the last call ends, so hasConnectionServiceFocus() stayed true with no connection left (still true 5 s after the end). deinitConnection resets it when the last connection goes; the native test now ends the call and checks it. - setupNative leaves the service available, as setup() does once the phone account is registered (it sets false first, then true); a comment says so. - docs/setup.md: once native code registers its own receiver, CallKeep's broadcasts reach it and are no longer kept for getInitialEvents(). - docs/events.md: iOS posts each notification on the thread that sends the event, CallKit's private queue for didDisplayIncomingCall; observe with queue: .main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sboily
force-pushed
the
feat/native-incoming-calls
branch
from
October 17, 2026 21:22
5bbf393 to
1bb0157
Compare
Only the connection service's events (didDisplayIncomingCall, createIncomingConnectionFailed, checkReachability, and an outgoing call's ongoing-call and audio-session events) are kept for getInitialEvents() when no receiver gets them. The call's actions, sent by VoiceConnection (showIncomingCallUi, answer, end, hold, DTMF, audio route, silence), reach only the receivers registered at the time: a call shown natively with no receiver, then React Native started, gives JavaScript only didDisplayIncomingCall (checked on an Android 10 emulator). The docs said every broadcast was kept when no receiver got it, and now tell native code to register its receiver before displayIncomingCall.
This was referenced Oct 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A call push can wake an app whose JavaScript isn't running or isn't ready, yet the call has to ring at once. Luge Mobile shows its calls from native code on both platforms, with this code patched onto 4.4.0. This PR proposes it upstream.
Rebased on master with #904 and #905. #904 is what lets the native path use a
foregroundService: the service reads its settings itself and no longer needs the module.Android (self-managed, Android 8 and later)
RNCallKeepModule.setupNative(context, options)setup()does, without creating a module or a React context.setup()ends in once the account is registered (setup()sets it unavailable first, then available).IllegalArgumentExceptionunlessselfManagedistrueon Android 8+.RNCallKeepModule.displayIncomingCall(context, uuid, number, callerName, hasVideo)IllegalStateExceptionbefore any setup in the process.getInitialEvents()when React Native starts (documented).VoiceConnectionService.nativeFocusListenerandhasConnectionServiceFocus()onConnectionServiceFocusLostwhen the last call ends, sodeinitConnectionresets the state when the last connection goes.To get there,
initializeTelecomManager,registerPhoneAccount,storeSettings,getApplicationNameandisSelfManagedbecome static and take the context as an argument; the JavaScript path calls them as before.iOS
sendEventWithNameWrapperalso posts each event toNotificationCenter, under its native name (RNCallKeepPerformAnswerCallAction, ...), with the body asuserInfo. Native code can then follow the call's actions before a React Native runtime exists, or while JavaScript is busy, as Android's local broadcasts already allow. The JavaScript events are unchanged.The notification is posted on the thread that sends the event, which can be CallKit's private queue (
didDisplayIncomingCall). The docs recommend observing withqueue: .main.Docs
docs/setup.md: Setup from native code (Android).docs/events.md: Listening from native code (iOS), with the notification names and the thread.CHANGELOG.md: updated.Tests
tests/android/CallKeepNativeSetupTest.kt, run in the example app (React Native 0.87.1) on a Galaxy S24 with Android 16, passes:deinitConnection, it fails with "the call focus outlived the last call".It doesn't compile on master, since the API is new.
Lint and checks:
./gradlew :react-native-callkeep:lintDebugreports no errors, andnpm run checkpasses.Luge Mobile: on the same S24, its 8 Android instrumentation classes pass with master plus fix(android): the foreground service keeps an app's channel and starts without the module #904, feat(android): foregroundServiceType and endCallsOnHostDestroy options #905 and this branch (native incoming call, silence, answer, hold, focus loss, end). That run used the first version of these branches.
After the rebase: this test,
CallKeepCallLifetimeOptionsTestand fix(android): the foreground service keeps an app's channel and starts without the module #904's twoCallKeepForegroundServiceTestcases pass in the example app.iOS:
didDisplayIncomingCall, with thecallUUIDand the payload, before the module exists, and JavaScript still receives it afterwards. On master the observer receives nothing.🤖 Generated with Claude Code