Skip to content

[Bug]: Android: cleanup() never calls Navigator#cleanup(), leaving a 1Hz location request active for the app process lifetime #634

Description

@miguelmiesion

Is there an existing issue for this?

  • I have searched the existing issues

Description of the bug

On Android, NavModule.cleanup() removes listeners, stops guidance, clears destinations, and nulls out the module's mNavigator reference, but never invokes the native Navigator#cleanup(). Since the Navigator is a singleton held by NavigationApi, dropping the module's reference doesn't destroy it: its internal location listener keeps running for the lifetime of the app process.

Observable effect after cleanup() resolves successfully from JS:

  • A sustained ~1Hz GPS request remains registered with the fused location provider (visible via adb shell dumpsys location).
  • The system location-in-use indicator stays on whenever the app is in the foreground — on any screen, even outside the NavigationProvider.
  • The request re-registers on every return to foreground, and only dies when the process is killed.

This causes unnecessary battery drain and signals location usage to the user after the navigation session has ended.

Expected behavior: after cleanup() resolves, the Navigator's location request should be released and the location indicator should turn off.

Google's instance cleanup best practices recommend calling Navigator#cleanup when the navigation session is completed.

Proposed fix: adding the missing call at the end of the teardown block in NavModule.cleanup() resolves it. The method already captures a local final Navigator navigator reference before nulling out mNavigator, so the call fits the existing pattern:

navigator.getSimulator().unsetUserLocation();
navigator.cleanup();  // <-- missing call
promise.resolve(true);

Verified on a physical Android device: with this change the FLP request is released immediately when cleanup() resolves and the location indicator turns off. Subsequent init() → set destination → startGuidance()cleanup() cycles work correctly, including backgrounding/foregrounding between cycles.

Possibly related history: #34 addressed guidance continuing after unmount, and #566 fixed navinfo forwarding surviving cleanup(). This appears to be the same class of incomplete-teardown issue, but affecting the Navigator's own location provider. If Navigator#cleanup() was omitted intentionally (e.g. to work around a known race condition on re-initialization), happy to hear the context.

iOS Platform

Not verified

Android Platform

Affected

React Native version

0.83.10

React version

19.2.0

Package version

0.16.3

Native SDK versions

  • I haven't changed the version of the native SDKs

React Native Doctor Output

npx react-native doctor reports only environment-detection issues unrelated
to this bug (it doesn't find Android Studio/SDK paths on Fedora, though
ANDROID_HOME, adb, JDK and Gradle all resolve correctly and builds run fine).

Output of npx expo-env-info:

System:
OS: Linux (Fedora 43)
Node: 24.14.1
npm: 11.11.0
npmPackages:
expo: 55.0.28
expo-router: 55.0.17
react: 19.2.0
react-native: 0.83.10
npmGlobalPackages:
eas-cli: 21.4.0
Expo Workflow: bare (prebuild)

@googlemaps/react-native-navigation-sdk: 0.16.3
Test device: Android [16] ([moto g75 5G]), physical device

Steps to reproduce

  1. In a screen wrapped by NavigationProvider, call navigationController.init() (accepting the Terms and Conditions dialog).
  2. Call navigationController.cleanup() — the promise resolves successfully.
  3. Run adb shell dumpsys location and inspect the fused location provider section: a sustained ~1Hz GPS request is still being delivered, and the system location-in-use indicator remains visible while the app is in the foreground.
  4. Navigate to any other screen, including screens outside the NavigationProvider — the indicator stays on.
  5. Send the app to background (the request stops) and return to foreground — the request re-registers and the indicator comes back. It only stops for good when the app process is killed.

Expected vs Actual Behavior

Expected: after cleanup() resolves, the Navigator should be destroyed and its location request released — the fused location provider request should stop and the system location-in-use indicator should turn off.

Actual: cleanup() resolves with true, but the Navigator singleton is never destroyed (only the module's reference is nulled out). A ~1Hz GPS request stays registered with the fused location provider for the lifetime of the app process, keeping the location indicator on across the whole app whenever it is in the foreground.

Logs: no errors or exceptions are thrown on either side — cleanup() resolves successfully and logcat shows nothing unusual, which is what makes this hard to notice. The only visible evidence is in adb shell dumpsys location, where the GPS provider keeps delivering to the fused location provider at ~1Hz after cleanup:

08-19 11:24:19.784: received GPS locations[1]
08-19 11:24:19.785: delivered locations[1] to <uid>/com.google.android.gms[fused_location_provider]/<id>
08-19 11:24:20.786: received GPS locations[1]
08-19 11:24:20.788: delivered locations[1] to <uid>/com.google.android.gms[fused_location_provider]/<id>

These deliveries continue indefinitely while the app is foregrounded, stop when the app is backgrounded, and resume on returning to foreground. After adding navigator.cleanup() to NavModule.cleanup(), they stop immediately when cleanup resolves.

Code Sample

Minimal reproduction — no destination, no guidance, no NavigationView needed:

import { View, Button } from 'react-native';
import { NavigationProvider, useNavigation } from '@googlemaps/react-native-navigation-sdk';

function Repro() {
  const { navigationController } = useNavigation();

  return (
    <View style={{ flex: 1, justifyContent: 'center', gap: 20, padding: 40 }}>
      <Button title="INIT + GUIDANCE" onPress={async () => {
  await navigationController.init();
  await navigationController.setDestinations([
    { title: 'Test', position: { lat: TU_LAT, lng: TU_LNG } },
  ]);
  await navigationController.startGuidance();
  console.log('[repro] guidance started');
}} />
      <Button title="CLEANUP" onPress={async () => {
        await navigationController.cleanup();
        console.log('[repro] cleanup OK');
      }} />
    </View>
  );
}

export default function ReproScreen() {
  return (
    <NavigationProvider termsAndConditionsDialogOptions={{ title: 'Repro' }}>
      <Repro />
    </NavigationProvider>
  );
}

Press "init" (accept the Terms and Conditions dialog), then press "cleanup". Both resolve successfully, but the location request persists.

Additional Context

No response

Metadata

Metadata

Labels

triage meI really want to be triaged.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions