Skip to content

fix(android): prevent TurboModule event emitter crash before setEventEmitterCallback (#428)#441

Open
AmitKulkarni23 wants to merge 1 commit into
radarlabs:masterfrom
AmitKulkarni23:master
Open

fix(android): prevent TurboModule event emitter crash before setEventEmitterCallback (#428)#441
AmitKulkarni23 wants to merge 1 commit into
radarlabs:masterfrom
AmitKulkarni23:master

Conversation

@AmitKulkarni23
Copy link
Copy Markdown

@AmitKulkarni23 AmitKulkarni23 commented May 13, 2026

The Issue

On React Native 0.79+ with newArchEnabled=true (TurboModules / New Architecture), the app crashes on Android during initialization. The Radar SDK's native receiver callbacks (onClientLocationUpdated, onEventsReceived, etc.) can fire before the TurboModule framework has called setEventEmitterCallback, leaving mEventEmitterCallback as null. Calling any emitXxx method at that point crashes the process.

Affected: Android + New Architecture only. iOS was fixed in #433. Old Architecture unaffected. This PR addresses #428.


Steps to Reproduce

Setup

  1. Create a fresh React Native 0.81 app with New Architecture enabled:
npx @react-native-community/cli init RadarCrashTest --version 0.81.0
  1. Confirm android/gradle.properties has:
newArchEnabled=true
  1. Build and install the SDK as a local tarball:
# In the react-native-radar repo
npm install
npm run build-all
npm pack
# → react-native-radar-x.y.z.tgz

# In the test app
npm install /path/to/react-native-radar-x.y.z.tgz
  1. Add location permissions to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
  1. Add Google Play Services location dependency to android/app/build.gradle (required on emulator — the Radar SDK declares this as a transitive dep but it must be resolvable at runtime):
implementation("com.google.android.gms:play-services-location:21.3.0")
  1. Use a Google Play AVD image (not just Google APIs) so GMS is available on the emulator.

Workarounds Applied During Reproduction

Two bundler issues required patching node_modules/react-native-radar/dist/ directly for the test app to load:

a) dist/index.js — conditional UI requires caused bundler errors:

// Before
let Autocomplete = Platform.OS !== "web" ? require("./ui/autocomplete").default : {};
let Map = Platform.OS !== "web" ? require("./ui/map").default : {};

// After (replaced with empty objects to unblock bundler)
let Autocomplete = {};
let Map = {};

b) If npm run build-all was not run before npm pack, dist/ui/map.jsx would be missing. Fix: render an empty view in dist/ui/map.jsx. This is avoided by always running build-all before packing.

Triggering the Crash

In App.tsx, call initialize and trackOnce on mount:

useEffect(() => {
  Radar.initialize('prj_test_pk_fake_key_crash_repro');
  Radar.trackOnce();
}, []);

To make the crash deterministic without waiting for a real GPS fix, an init block was injected into RadarModule.kt to emit an event during construction — before setEventEmitterCallback is called by the framework:

// Injected for reproduction only — not part of the fix
init {
    val testBlob = Arguments.createMap().apply { putString("test", "crash") }
    emitClientLocationEmitter(testBlob)
}

Build & Run

npm run android
adb logcat | grep -E "FATAL|RadarModule|__next_prime"

Crash Log

Captured on: Pixel 8 emulator, API 36.1 (Google Play ARM64), React Native 0.81, newArchEnabled=true

05-12 19:00:56.742  5693  5791 F .radarcrashtest: java_vm_ext.cc:620] JNI DETECTED ERROR IN APPLICATION: JNI GetObjectRefType called with pending exception java.lang.NullPointerException: Attempt to invoke virtual method 'void com.facebook.react.bridge.CxxCallbackImpl.invoke(java.lang.Object[])' on a null object reference
05-12 19:00:56.742  5693  5791 F .radarcrashtest: java_vm_ext.cc:620]   at void com.radar.NativeRadarSpec.emitClientLocationEmitter(com.facebook.react.bridge.ReadableMap) (NativeRadarSpec.java:43)
05-12 19:00:56.742  5693  5791 F .radarcrashtest: java_vm_ext.cc:620]   at void com.radar.RadarModule.<init>(com.facebook.react.bridge.ReactApplicationContext) (RadarModule.kt:48)
05-12 19:00:56.742  5693  5791 F .radarcrashtest: java_vm_ext.cc:620]   at com.facebook.react.bridge.NativeModule com.radar.RadarPackage.getModule(java.lang.String, com.facebook.react.bridge.ReactApplicationContext) (RadarPackage.kt:13)
05-12 19:00:56.742  5693  5791 F .radarcrashtest: java_vm_ext.cc:620]   at com.facebook.react.bridge.NativeModule com.facebook.react.ReactPackageTurboModuleManagerDelegate.initialize$lambda$0(com.facebook.react.ReactPackage, com.facebook.react.bridge.ReactApplicationContext, java.lang.String) (ReactPackageTurboModuleManagerDelegate.kt:56)
05-12 19:00:56.742  5693  5791 F .radarcrashtest: java_vm_ext.cc:620]   at com.facebook.react.internal.turbomodule.core.TurboModuleManager.getOrCreateModule(java.lang.String, com.facebook.react.internal.turbomodule.core.TurboModuleManager$ModuleHolder, boolean) (TurboModuleManager.kt:242)
05-12 19:00:56.742  5693  5791 F .radarcrashtest: java_vm_ext.cc:620]   at com.facebook.react.internal.turbomodule.core.TurboModuleManager.getTurboJavaModule(java.lang.String) (TurboModuleManager.kt:159)
05-12 19:00:56.742  5693  5791 F .radarcrashtest: java_vm_ext.cc:620]   at void com.facebook.jni.NativeRunnable.run() (NativeRunnable.java:-2)
05-12 19:00:56.742  5693  5791 F .radarcrashtest: java_vm_ext.cc:620]   at void java.lang.Thread.run() (Thread.java:1563)
05-12 19:00:57.031  5693  5791 F .radarcrashtest: runtime.cc:714] Runtime aborting...
05-12 19:00:57.032  5693  5791 F .radarcrashtest: runtime.cc:722]   at void com.radar.RadarModule.<init>(com.facebook.react.bridge.ReactApplicationContext) (RadarModule.kt:48)
05-12 19:00:57.650  5804  5804 F DEBUG   :   at void com.radar.RadarModule.<init>(com.facebook.react.bridge.ReactApplicationContext) (RadarModule.kt:48)

Root cause: mEventEmitterCallback (set by the TurboModule framework via setEventEmitterCallback) is null when a receiver callback fires early. Any emitXxx call at this point invokes CxxCallbackImpl.invoke() on a null reference, aborting the runtime.


The Fix

File: android/src/newarch/java/com/radar/RadarModule.kt

Add a @Volatile flag that is set to true only after setEventEmitterCallback is called by the framework. Guard every emitXxx call site with an early return on this flag.

@Volatile provides the Java memory model guarantee needed here: once the framework thread writes true, any background thread (GPS callbacks, SDK receivers) is guaranteed to see the updated value and sees mEventEmitterCallback as fully initialized.

@Volatile private var jsEventEmitterReady = false

override fun setEventEmitterCallback(eventEmitterCallback: CxxCallbackImpl) {
    super.setEventEmitterCallback(eventEmitterCallback)
    jsEventEmitterReady = true
}

Applied to all receiver callbacks:

This matches the approach used for iOS in #433 (jsEventEmitterReady bool + setEventEmitterCallback: override inside #ifdef RCT_NEW_ARCH_ENABLED).


Verification

After applying the fix, rebuilt and reinstalled the APK on the same emulator with the same test app (including the injected init block that previously triggered the crash).

adb logcat | grep -E "FATAL|RadarModule|__next_prime"

No crash entries after the fix was deployed. All pre-fix crash lines confirm the before/after boundary:

  • Before fix: FATAL at RadarModule.<init> (RadarModule.kt:48) — consistent across multiple runs at 18:48, 18:55, 19:00
  • After fix: no FATAL, no RadarModule crash lines in logcat

Note to Reviewers/Maintainers

First-time contributor here. I used Claude (Anthropic's AI assistant) to help root-cause but I’ve spent significant time manually verifying the mechanics of the fix. I’ve ensured that the @volatile implementation correctly addresses the TurboModule init sequence (specifically handling the different failure modes on Android). I've also completed a full reproduction and manual testing pass on an emulator. Happy to answer questions or make changes based on feedback.

@alanjcharles14
Copy link
Copy Markdown
Contributor

@AmitKulkarni23 thanks so much for taking the time to make this PR and include such a thorough description. Generally, I think this makes sense and seems pretty safe for us to add.

I am curious to better understand how you came across this, if only for my own comprehension. I am able to build the SDK on the new architecture for android using a typical npm download/build approach, which is why I haven't seen it in a typical development flow.

@AmitKulkarni23
Copy link
Copy Markdown
Author

AmitKulkarni23 commented May 18, 2026

Thanks @alanjcharles14. You are right that this doesn't surface via a typical npm install + build. It's a race condition (timing issue).

I was building a proof-of-concept around HorizonDB and exploring Radar's public repos as part of that. Saw issue #428 was open, noticed the iOS fix had already landed in #433 using a jsEventEmitterReady guard, and the Android path seemed similar and a straightforward fix.

The hard part was reproducing this - Claude helped me a lot here.

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.

2 participants