Is there an existing issue for this?
Description of the bug
On Android, NavViewModule.moveCamera (and every other controller-backed method in NavViewModule.java) crashes the app with a NullPointerException when the call reaches the fragment after it has been created but before its MapViewController exists.
MapViewFragment and NavViewFragment create mMapViewController inside the getMapAsync callback, so there is a window between fragment creation and map-ready in which getMapController() returns null. NavViewModule only null-checks the fragment:
IMapViewFragment fragment = mNavViewManager.getFragmentByNativeId(nativeID);
if (fragment == null) {
promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE);
return;
}
fragment.getMapController().moveCamera(cameraPosition.toHashMap()); // NPE when the map is not ready yet
Because the runnable is posted with UiThreadUtil.runOnUiThread, the exception is thrown on the main looper and takes the whole app down. NavViewManager already guards this case in getMapControllerProperties (fragment.getMapController() != null), so the module is the only place that dereferences the controller without a check. main still has the same code as 0.17.1.
Production stack trace (Crashlytics, Android, @googlemaps/react-native-navigation-sdk@0.17.1):
Fatal Exception: java.lang.NullPointerException: Attempt to invoke virtual method 'void com.google.android.react.navsdk.m.C(java.util.HashMap)' on a null object reference
at com.google.android.react.navsdk.NavViewModule.lambda$moveCamera$9(NavViewModule.java:259)
at android.os.Handler.handleCallback(Handler.java:1095)
at android.os.Handler.dispatchMessageImpl(Handler.java:135)
at android.os.Handler.dispatchMessage(Handler.java:125)
at android.os.Looper.loopOnce(Looper.java:269)
at android.os.Looper.loop(Looper.java:367)
Line 259 is the fragment.getMapController().moveCamera(...) call; the obfuscated m.C(HashMap) is MapViewController.moveCamera.
iOS Platform
Android Platform
React Native version
0.86.3 (New Architecture)
React version
19.2.8
Package version
0.17.1
Native SDK versions
Android Navigation SDK 7.9.0 (from the package's android/build.gradle)
React Native Doctor Output
Not relevant to this report: the crash is a plain null dereference in the module source and reproduces on a stock emulator.
Steps to reproduce
- Mount a
MapView or NavigationView.
- From JS, call any controller-backed
NavViewModule method for that view (moveCamera, addMarker, setZoomLevel, setFollowingPerspective, ...) after the native view has mounted but before onMapReady fires. The controller returned by onMapViewControllerCreated is handed out at mount time, so this is easy to hit when an app remounts a view (for example swapping MapView and NavigationView) and reuses readiness state from the previous instance.
- If the call lands after the fragment transaction has committed and before the
getMapAsync callback has run, the app crashes with the trace above.
To land a call in that window deliberately on an emulator, poll every 5 ms right after mounting the view with a fragment-only method and moveCamera back to back: setNavigationUIEnabled resolving (fragment exists) while moveCamera rejects or crashes shows the call arrived inside the window. In our runs the window was open at about 50 ms after mount on a Pixel-class emulator; it is wider on real devices under load, which is where the production crashes come from.
Expected vs Actual Behavior
Expected: the promise rejects with NO_MAP_ERROR_CODE, the same way it does when the fragment does not exist yet.
Actual: NullPointerException on the main thread, fatal for the app.
Code Sample
Minimal fix we are shipping as a patch, applied to every method in NavViewModule.java that dereferences getMapController() (19 methods):
- if (fragment == null) {
+ if (fragment == null || fragment.getMapController() == null) {
promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE);
return;
}
Verified on device: with the guard, a moveCamera that arrives while setNavigationUIEnabled already succeeds for the same view comes back as a NO_MAP_ERROR_CODE rejection and the app keeps running.
Additional Context
Happy to open a PR with the change above if that helps.
Is there an existing issue for this?
Description of the bug
On Android,
NavViewModule.moveCamera(and every other controller-backed method inNavViewModule.java) crashes the app with aNullPointerExceptionwhen the call reaches the fragment after it has been created but before itsMapViewControllerexists.MapViewFragmentandNavViewFragmentcreatemMapViewControllerinside thegetMapAsynccallback, so there is a window between fragment creation and map-ready in whichgetMapController()returnsnull.NavViewModuleonly null-checks the fragment:Because the runnable is posted with
UiThreadUtil.runOnUiThread, the exception is thrown on the main looper and takes the whole app down.NavViewManageralready guards this case ingetMapControllerProperties(fragment.getMapController() != null), so the module is the only place that dereferences the controller without a check.mainstill has the same code as 0.17.1.Production stack trace (Crashlytics, Android,
@googlemaps/react-native-navigation-sdk@0.17.1):Line 259 is the
fragment.getMapController().moveCamera(...)call; the obfuscatedm.C(HashMap)isMapViewController.moveCamera.iOS Platform
Android Platform
React Native version
0.86.3 (New Architecture)
React version
19.2.8
Package version
0.17.1
Native SDK versions
Android Navigation SDK 7.9.0 (from the package's
android/build.gradle)React Native Doctor Output
Not relevant to this report: the crash is a plain null dereference in the module source and reproduces on a stock emulator.
Steps to reproduce
MapVieworNavigationView.NavViewModulemethod for that view (moveCamera,addMarker,setZoomLevel,setFollowingPerspective, ...) after the native view has mounted but beforeonMapReadyfires. The controller returned byonMapViewControllerCreatedis handed out at mount time, so this is easy to hit when an app remounts a view (for example swappingMapViewandNavigationView) and reuses readiness state from the previous instance.getMapAsynccallback has run, the app crashes with the trace above.To land a call in that window deliberately on an emulator, poll every 5 ms right after mounting the view with a fragment-only method and
moveCameraback to back:setNavigationUIEnabledresolving (fragment exists) whilemoveCamerarejects or crashes shows the call arrived inside the window. In our runs the window was open at about 50 ms after mount on a Pixel-class emulator; it is wider on real devices under load, which is where the production crashes come from.Expected vs Actual Behavior
Expected: the promise rejects with
NO_MAP_ERROR_CODE, the same way it does when the fragment does not exist yet.Actual:
NullPointerExceptionon the main thread, fatal for the app.Code Sample
Minimal fix we are shipping as a patch, applied to every method in
NavViewModule.javathat dereferencesgetMapController()(19 methods):Verified on device: with the guard, a
moveCamerathat arrives whilesetNavigationUIEnabledalready succeeds for the same view comes back as aNO_MAP_ERROR_CODErejection and the app keeps running.Additional Context
Happy to open a PR with the change above if that helps.