Skip to content

Commit 436716c

Browse files
authored
fix: stop naviinfo forwarding on cleanup (#566)
1 parent ca3e4e6 commit 436716c

File tree

7 files changed

+154
-6
lines changed

7 files changed

+154
-6
lines changed

android/src/main/java/com/google/android/react/navsdk/NavForwardingManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public static void startNavForwarding(
3535

3636
/** Unregisters the service receiving navigation updates */
3737
public static void stopNavForwarding(
38-
Navigator navigator, Context context, INavigationCallback navigationCallback) {
38+
Navigator navigator, INavigationCallback navigationCallback) {
3939
// Unregister the nav info receiving service.
4040
boolean success = navigator.unregisterServiceForNavUpdates();
4141
if (success) {

android/src/main/java/com/google/android/react/navsdk/NavModule.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public class NavModule extends NativeNavModuleSpec
8181
private Navigator.TrafficUpdatedListener mTrafficUpdatedListener;
8282
private Navigator.ReroutingListener mReroutingListener;
8383
private Navigator.RemainingTimeOrDistanceChangedListener mRemainingTimeOrDistanceChangedListener;
84+
private Observer<NavInfo> mNavInfoObserver;
8485

8586
private @Navigator.TaskRemovedBehavior int taskRemovedBehaviour =
8687
Navigator.TaskRemovedBehavior.CONTINUE_SERVICE;
@@ -174,15 +175,22 @@ public void cleanup(final Promise promise) {
174175
}
175176

176177
final Navigator navigator = mNavigator;
178+
177179
UiThreadUtil.runOnUiThread(
178180
() -> {
179181
// Remove listeners on UI thread to serialize with callback dispatch.
180182
// This reduces the chance of triggering a race condition in the Navigation SDK
181183
// where callbacks may still be in-flight during removal.
182184
removeLocationListener();
183185
removeNavigationListeners();
184-
navigator.clearDestinations();
186+
removeNavInfoObserver();
187+
// Null out fields after listener removal so the removal methods
188+
// can still access mNavigator and mRoadSnappedLocationProvider.
189+
mNavigator = null;
190+
mRoadSnappedLocationProvider = null;
191+
NavForwardingManager.stopNavForwarding(navigator, this);
185192
navigator.stopGuidance();
193+
navigator.clearDestinations();
186194
navigator.getSimulator().unsetUserLocation();
187195
promise.resolve(true);
188196
});
@@ -279,14 +287,15 @@ public void initializeNavigationSession(
279287
initializeNavigationApi();
280288

281289
// Observe live data for nav info updates.
282-
Observer<NavInfo> navInfoObserver = this::showNavInfo;
283-
290+
// Remove any existing observer first to prevent duplicates after cleanup+reinit cycles.
284291
UiThreadUtil.runOnUiThread(
285292
() -> {
293+
removeNavInfoObserver();
294+
mNavInfoObserver = this::showNavInfo;
286295
final Activity currentActivity = getReactApplicationContext().getCurrentActivity();
287296
if (currentActivity != null) {
288297
NavInfoReceivingService.getNavInfoLiveData()
289-
.observe((LifecycleOwner) currentActivity, navInfoObserver);
298+
.observe((LifecycleOwner) currentActivity, mNavInfoObserver);
290299
}
291300
});
292301
}
@@ -418,7 +427,7 @@ public void setTurnByTurnLoggingEnabled(boolean isEnabled) {
418427
if (isEnabled) {
419428
NavForwardingManager.startNavForwarding(mNavigator, currentActivity, this);
420429
} else {
421-
NavForwardingManager.stopNavForwarding(mNavigator, currentActivity, this);
430+
NavForwardingManager.stopNavForwarding(mNavigator, this);
422431
}
423432
}
424433

@@ -528,6 +537,13 @@ private void removeNavigationListeners() {
528537
}
529538
}
530539

540+
private void removeNavInfoObserver() {
541+
if (mNavInfoObserver != null) {
542+
NavInfoReceivingService.getNavInfoLiveData().removeObserver(mNavInfoObserver);
543+
mNavInfoObserver = null;
544+
}
545+
}
546+
531547
private void createWaypoint(Map map) {
532548
String placeId = CollectionUtil.getString("placeId", map);
533549
String title = CollectionUtil.getString("title", map);

example/e2e/navigation.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,12 @@ describe('Navigation tests', () => {
9898
await expectNoErrors();
9999
await expectSuccess();
100100
});
101+
102+
it('NT10 - test navInfo events are restored after cleanup and re-init', async () => {
103+
await selectTestByName('testNavInfoEventsAfterCleanup');
104+
await agreeToTermsAndConditions();
105+
await waitForTestToFinish();
106+
await expectNoErrors();
107+
await expectSuccess();
108+
});
101109
});

example/src/screens/IntegrationTestsScreen.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
testMapStyle,
5858
testMinMaxZoomLevels,
5959
testSetFollowingPerspective,
60+
testNavInfoEventsAfterCleanup,
6061
NO_ERRORS_DETECTED_LABEL,
6162
} from './integration_tests/integration_test';
6263

@@ -91,6 +92,7 @@ const IntegrationTestsScreen = () => {
9192
setOnLocationChanged,
9293
setOnRemainingTimeOrDistanceChanged,
9394
setOnRouteChanged,
95+
setOnTurnByTurn,
9496
} = useNavigation();
9597

9698
const [detoxStepNumber, setDetoxStepNumber] = useState(0);
@@ -229,6 +231,7 @@ const IntegrationTestsScreen = () => {
229231
setOnRemainingTimeOrDistanceChanged,
230232
setOnRouteChanged,
231233
setOnLocationChanged,
234+
setOnTurnByTurn,
232235
passTest,
233236
failTest,
234237
setDetoxStep,
@@ -321,6 +324,9 @@ const IntegrationTestsScreen = () => {
321324
case 'testSetFollowingPerspective':
322325
await testSetFollowingPerspective(getTestTools());
323326
break;
327+
case 'testNavInfoEventsAfterCleanup':
328+
await testNavInfoEventsAfterCleanup(getTestTools());
329+
break;
324330
default:
325331
resetTestState();
326332
break;
@@ -550,6 +556,13 @@ const IntegrationTestsScreen = () => {
550556
}}
551557
testID="testSetFollowingPerspective"
552558
/>
559+
<ExampleAppButton
560+
title="testNavInfoEventsAfterCleanup"
561+
onPress={() => {
562+
runTest('testNavInfoEventsAfterCleanup');
563+
}}
564+
testID="testNavInfoEventsAfterCleanup"
565+
/>
553566
</OverlayModal>
554567
</View>
555568
);

example/src/screens/integration_tests/integration_test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
type NavigationController,
2929
type NavigationViewController,
3030
type TimeAndDistance,
31+
type TurnByTurnEvent,
3132
} from '@googlemaps/react-native-navigation-sdk';
3233
import { Platform } from 'react-native';
3334
import { delay, roundDown } from './utils';
@@ -48,6 +49,9 @@ interface TestTools {
4849
setOnLocationChanged: (
4950
listener: ((location: Location) => void) | null | undefined
5051
) => void;
52+
setOnTurnByTurn: (
53+
listener: ((turnByTurnEvents: TurnByTurnEvent[]) => void) | null | undefined
54+
) => void;
5155
passTest: () => void;
5256
failTest: (message: string) => void;
5357
setDetoxStep: (stepNumber: number) => void;
@@ -1866,3 +1870,104 @@ export const testSetFollowingPerspective = async (testTools: TestTools) => {
18661870

18671871
await initializeNavigation(navigationController, failTest);
18681872
};
1873+
1874+
/**
1875+
* Tests that navInfo (turn-by-turn) events can be received after performing
1876+
* a cleanup and re-initialization cycle. This verifies that the NavForwardingManager
1877+
* and LiveData observer are properly restored after cleanup.
1878+
*/
1879+
export const testNavInfoEventsAfterCleanup = async (testTools: TestTools) => {
1880+
const {
1881+
navigationController,
1882+
setOnNavigationReady,
1883+
setOnLocationChanged,
1884+
setOnTurnByTurn,
1885+
passTest,
1886+
failTest,
1887+
} = testTools;
1888+
1889+
// Accept ToS first
1890+
if (!(await acceptToS(navigationController, failTest))) {
1891+
return;
1892+
}
1893+
1894+
const startLocation: LatLng = {
1895+
lat: 37.79136614772824,
1896+
lng: -122.41565900473043,
1897+
};
1898+
1899+
const destination = {
1900+
title: 'Grace Cathedral',
1901+
position: {
1902+
lat: 37.791957,
1903+
lng: -122.412529,
1904+
},
1905+
};
1906+
1907+
let phase: 'first' | 'second' = 'first';
1908+
1909+
setOnTurnByTurn(async (_events: TurnByTurnEvent[]) => {
1910+
if (phase === 'first') {
1911+
// Received navInfo in first session — now cleanup and re-init
1912+
phase = 'second';
1913+
setOnTurnByTurn(null);
1914+
1915+
await navigationController.cleanup();
1916+
1917+
// Re-initialize after cleanup
1918+
setOnNavigationReady(async () => {
1919+
disableVoiceGuidanceForTests(navigationController);
1920+
navigationController.setTurnByTurnLoggingEnabled(true);
1921+
1922+
const located2 = await simulateAndWaitForLocation(
1923+
navigationController,
1924+
setOnLocationChanged,
1925+
startLocation
1926+
);
1927+
if (!located2) {
1928+
return failTest(
1929+
'Timed out waiting for simulated location after re-init'
1930+
);
1931+
}
1932+
await navigationController.setDestination(destination);
1933+
await navigationController.startGuidance();
1934+
await navigationController.simulator.simulateLocationsAlongExistingRoute(
1935+
{ speedMultiplier: 5 }
1936+
);
1937+
1938+
// Listen for turn-by-turn events in the second session
1939+
setOnTurnByTurn(async () => {
1940+
// Received navInfo after cleanup+reinit — test passes
1941+
setOnTurnByTurn(null);
1942+
await navigationController.cleanup();
1943+
passTest();
1944+
});
1945+
});
1946+
1947+
await initializeNavigation(navigationController, failTest);
1948+
}
1949+
});
1950+
1951+
setOnNavigationReady(async () => {
1952+
disableVoiceGuidanceForTests(navigationController);
1953+
navigationController.setTurnByTurnLoggingEnabled(true);
1954+
1955+
const located = await simulateAndWaitForLocation(
1956+
navigationController,
1957+
setOnLocationChanged,
1958+
startLocation
1959+
);
1960+
if (!located) {
1961+
return failTest(
1962+
'Timed out waiting for simulated location to be confirmed'
1963+
);
1964+
}
1965+
await navigationController.setDestination(destination);
1966+
await navigationController.startGuidance();
1967+
await navigationController.simulator.simulateLocationsAlongExistingRoute({
1968+
speedMultiplier: 5,
1969+
});
1970+
});
1971+
1972+
await initializeNavigation(navigationController, failTest);
1973+
};

ios/react-native-navigation-sdk/NavModule.mm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,14 @@ - (void)initializeSession {
104104
_session.started = YES;
105105

106106
if (self->_session.navigator) {
107+
// Remove any existing listener first to prevent duplicates
108+
// in case initializeSession is called multiple times.
109+
[self->_session.navigator removeListener:self];
107110
[self->_session.navigator addListener:self];
108111
self->_session.navigator.stopGuidanceAtArrival = NO;
109112
}
110113

114+
[self->_session.roadSnappedLocationProvider removeListener:self];
111115
[self->_session.roadSnappedLocationProvider addListener:self];
112116

113117
NavViewModule *navViewModule = [NavViewModule sharedInstance];
@@ -277,6 +281,7 @@ - (void)cleanup:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)re
277281
[self->_session.roadSnappedLocationProvider removeListener:self];
278282
}
279283

284+
self.enableUpdateInfo = NO;
280285
self->_session.started = NO;
281286
self->_session = nil;
282287

scripts/addlicense.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ addlicense -f header_template.txt $@ \
2626
--ignore "coverage/**" \
2727
--ignore ".yarn/**" \
2828
--ignore ".github/ISSUE_TEMPLATE/**" \
29+
--ignore ".github/java-upgrade/**" \
2930
.

0 commit comments

Comments
 (0)