Skip to content

fix(android): recover the location puck from a non-finite bearing - #4445

Open
ajtrasatti wants to merge 1 commit into
rnmapbox:mainfrom
ajtrasatti:fix/android-non-finite-puck-bearing
Open

ajtrasatti wants to merge 1 commit into
rnmapbox:mainfrom
ajtrasatti:fix/android-non-finite-puck-bearing

Conversation

@ajtrasatti

@ajtrasatti ajtrasatti commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Description

On Android, re-applying LocationPuck settings after the puck has received a NaN or Infinity bearing crashes the app at StyleManager.addPersistentStyleLayer with IllegalArgumentException: Parse error at offset 165: Missing a colon after a name of object member. This is the Maps SDK bug mapbox/mapbox-maps-android#2340 and still reproduces with Maps SDK 11.33.0.

RNMBXNativeUserLocation now applies its settings through LocationPuckGuard. When adding the puck layer fails, it resets the location component through LocationComponentManager and retries once, if location is requested and the map is running. Otherwise, or if the retry fails, it disables the component and resets it on the next puck apply while location is requested and the map is running. The reset holds the location provider (#4225); unrelated exceptions still propagate.

Checklist

  • I've read CONTRIBUTING.md
  • I updated the doc/other generated code with running yarn generate in the root folder (no JS or API changes)
  • I have tested the new feature on /example app.
    • In V11 mode/ios (Android-only change)
    • In New Architecture mode/ios (Android-only change)
    • In V11 mode/android
    • In New Architecture mode/android
  • I added/updated a sample - if a new feature was implemented (/example) (bug fix; the reproducer is below)

Testing

/example release builds (New Architecture, Maps SDK 11.33.0) on an Android 11 x86 emulator, with the reproducer below:

Build "NaN heading" → "Change puck" "90° heading" → "Change puck"
main crash: FATAL EXCEPTION … Parse error at offset 165 —
this PR no crash, puck hidden, logs Failed to add the location puck, disabled the location component … puck shown again
Mechanism and limits
  • LocationPuckManager keeps the last bearing it receives, NaN included, and its animator then stays at NaN. Re-applying puck settings re-adds the puck layer with that bearing.
  • com.mapbox.bindgen.Value.toJson() swallows Gson's "Numeric values must be finite" and returns the half-written JSON, which ends in "bearing" at offset 165. The native parser rejects it.
  • We see this crash in production (Android 11, "OnePlus8Pro" profile as in fixed animationMode Type formatting #2340). We reproduced it by injecting a NaN heading; we have not identified what produces the bearing in production.
  • In the reproducer, CustomLocationProvider replays the NaN when the component re-registers, so the retry fails and the puck stays hidden until the heading is finite.
  • Camera follow modes and LocationComponentManager settings changes still re-add the puck outside the guard. The proper fix is filtering non-finite values in the Maps SDK, as v10.20.0 did.
java.lang.IllegalArgumentException: Parse error at offset 165: Missing a colon after a name of object member.
	at com.mapbox.maps.StyleManager.addPersistentStyleLayer(Native Method)
	at com.mapbox.maps.MapboxStyleManager.addPersistentStyleLayer(MapboxStyleManager.kt:458)
	at com.mapbox.maps.plugin.locationcomponent.LocationLayerWrapper.bindTo(LocationLayerWrapper.kt:20)
	at com.mapbox.maps.plugin.locationcomponent.LocationComponentPositionManager.addLayerToMap(LocationComponentPositionManager.kt:31)
	at com.mapbox.maps.plugin.locationcomponent.LocationIndicatorLayerRenderer.addLayers(LocationIndicatorLayerRenderer.kt:41)
	at com.mapbox.maps.plugin.locationcomponent.LocationPuckManager.initialize(LocationPuckManager.kt:92)
	at com.mapbox.maps.plugin.locationcomponent.LocationPuckManager.updateSettings(LocationPuckManager.kt:125)
	at com.mapbox.maps.plugin.locationcomponent.LocationComponentPluginImpl.applySettings(LocationComponentPluginImpl.kt:388)
	at com.mapbox.maps.plugin.locationcomponent.generated.LocationComponentSettingsBase.setLocationPuck(LocationComponentSettingsBase.kt:224)
	at com.rnmapbox.rnmbx.components.location.RNMBXNativeUserLocation._apply(RNMBXNativeUserLocation.kt:153)
	at com.rnmapbox.rnmbx.components.location.RNMBXNativeUserLocation.setScale(RNMBXNativeUserLocation.kt:92)
Validation
  • ./gradlew :rnmapbox_maps:testDebugUnitTest from example/android: 27/27, including LocationPuckGuardTest (retry, stopped map, pending reset, unrelated exceptions) and LocationComponentResetTest (when the provider is held).
  • /example assembleRelease -PreactNativeArchitectures=x86, API 30 Google APIs emulator, results in the table above.

Component to reproduce the issue you're fixing

BugReportExample.js — tap "NaN heading", then "Change puck"
import React, { useState } from 'react';
import { Button, Text, View } from 'react-native';
import {
  Camera,
  CustomLocationProvider,
  LocationPuck,
  MapView,
} from '@rnmapbox/maps';

// "NaN heading" gives the puck a NaN bearing; "Change puck" re-adds its layer.
const coordinate = [-122.4193, 37.7793];
const styleJSON = JSON.stringify({
  version: 8,
  sources: {},
  layers: [
    { id: 'bg', type: 'background', paint: { 'background-color': '#e0e0e0' } },
  ],
});

const BugReportExample = () => {
  const [heading, setHeading] = useState(0);
  const [scale, setScale] = useState(1);

  return (
    <>
      <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
        <Button title="NaN heading" onPress={() => setHeading(Number.NaN)} />
        <Button title="90° heading" onPress={() => setHeading(90)} />
        <Button
          title="Change puck"
          onPress={() => setScale((value) => (value === 1 ? 1.5 : 1))}
        />
      </View>
      <Text>
        heading: {String(heading)} scale: {scale}
      </Text>
      <MapView style={{ flex: 1 }} styleJSON={styleJSON}>
        <Camera
          defaultSettings={{ centerCoordinate: coordinate, zoomLevel: 14 }}
        />
        <CustomLocationProvider coordinate={coordinate} heading={heading} />
        <LocationPuck
          puckBearingEnabled
          puckBearing="heading"
          scale={scale}
        />
      </MapView>
    </>
  );
};

export default BugReportExample;

@ajtrasatti
ajtrasatti force-pushed the fix/android-non-finite-puck-bearing branch from 83da888 to d1350fe Compare November 8, 2026 20:09
The Maps SDK location component keeps the last puck bearing it receives,
including NaN or Infinity. The next time the puck layer is added, for example
when LocationPuck settings change or its images load, addPersistentStyleLayer
throws "IllegalArgumentException: Parse error at offset 165: Missing a colon
after a name of object member." and the app crashes
(mapbox/mapbox-maps-android#2340).

RNMBXNativeUserLocation now applies its settings through LocationPuckGuard.
When adding the puck layer fails, it resets the location component through
LocationComponentManager, which drops the puck state holding the bad bearing,
and applies once more. The reset holds the location provider while giving up
location that is on (rnmapbox#4225) and is skipped while the map is stopped
(mapbox/mapbox-maps-android#2017). If the reset is skipped or the retry fails
too, the component stays disabled and the next puck apply resets it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ajtrasatti
ajtrasatti force-pushed the fix/android-non-finite-puck-bearing branch from d1350fe to 421043d Compare November 8, 2026 20:20
@ajtrasatti
ajtrasatti marked this pull request as ready for review November 8, 2026 20:55

This branch has not been deployed

No deployments
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