Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static void startNavForwarding(

/** Unregisters the service receiving navigation updates */
public static void stopNavForwarding(
Navigator navigator, Context context, INavigationCallback navigationCallback) {
Navigator navigator, INavigationCallback navigationCallback) {
// Unregister the nav info receiving service.
boolean success = navigator.unregisterServiceForNavUpdates();
if (success) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public class NavModule extends NativeNavModuleSpec
private Navigator.TrafficUpdatedListener mTrafficUpdatedListener;
private Navigator.ReroutingListener mReroutingListener;
private Navigator.RemainingTimeOrDistanceChangedListener mRemainingTimeOrDistanceChangedListener;
private Observer<NavInfo> mNavInfoObserver;

private @Navigator.TaskRemovedBehavior int taskRemovedBehaviour =
Navigator.TaskRemovedBehavior.CONTINUE_SERVICE;
Expand Down Expand Up @@ -174,15 +175,22 @@ public void cleanup(final Promise promise) {
}

final Navigator navigator = mNavigator;

UiThreadUtil.runOnUiThread(
() -> {
// Remove listeners on UI thread to serialize with callback dispatch.
// This reduces the chance of triggering a race condition in the Navigation SDK
// where callbacks may still be in-flight during removal.
removeLocationListener();
removeNavigationListeners();
navigator.clearDestinations();
removeNavInfoObserver();
// Null out fields after listener removal so the removal methods
// can still access mNavigator and mRoadSnappedLocationProvider.
mNavigator = null;
mRoadSnappedLocationProvider = null;
NavForwardingManager.stopNavForwarding(navigator, this);
navigator.stopGuidance();
navigator.clearDestinations();
navigator.getSimulator().unsetUserLocation();
promise.resolve(true);
});
Expand Down Expand Up @@ -279,14 +287,15 @@ public void initializeNavigationSession(
initializeNavigationApi();

// Observe live data for nav info updates.
Observer<NavInfo> navInfoObserver = this::showNavInfo;

// Remove any existing observer first to prevent duplicates after cleanup+reinit cycles.
UiThreadUtil.runOnUiThread(
() -> {
removeNavInfoObserver();
mNavInfoObserver = this::showNavInfo;
final Activity currentActivity = getReactApplicationContext().getCurrentActivity();
if (currentActivity != null) {
NavInfoReceivingService.getNavInfoLiveData()
.observe((LifecycleOwner) currentActivity, navInfoObserver);
.observe((LifecycleOwner) currentActivity, mNavInfoObserver);
}
});
}
Expand Down Expand Up @@ -418,7 +427,7 @@ public void setTurnByTurnLoggingEnabled(boolean isEnabled) {
if (isEnabled) {
NavForwardingManager.startNavForwarding(mNavigator, currentActivity, this);
} else {
NavForwardingManager.stopNavForwarding(mNavigator, currentActivity, this);
NavForwardingManager.stopNavForwarding(mNavigator, this);
}
}

Expand Down Expand Up @@ -528,6 +537,13 @@ private void removeNavigationListeners() {
}
}

private void removeNavInfoObserver() {
if (mNavInfoObserver != null) {
NavInfoReceivingService.getNavInfoLiveData().removeObserver(mNavInfoObserver);
mNavInfoObserver = null;
}
}

private void createWaypoint(Map map) {
String placeId = CollectionUtil.getString("placeId", map);
String title = CollectionUtil.getString("title", map);
Expand Down
8 changes: 8 additions & 0 deletions example/e2e/navigation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,12 @@ describe('Navigation tests', () => {
await expectNoErrors();
await expectSuccess();
});

it('NT10 - test navInfo events are restored after cleanup and re-init', async () => {
await selectTestByName('testNavInfoEventsAfterCleanup');
await agreeToTermsAndConditions();
await waitForTestToFinish();
await expectNoErrors();
await expectSuccess();
});
});
13 changes: 13 additions & 0 deletions example/src/screens/IntegrationTestsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
testMapStyle,
testMinMaxZoomLevels,
testSetFollowingPerspective,
testNavInfoEventsAfterCleanup,
NO_ERRORS_DETECTED_LABEL,
} from './integration_tests/integration_test';

Expand Down Expand Up @@ -91,6 +92,7 @@ const IntegrationTestsScreen = () => {
setOnLocationChanged,
setOnRemainingTimeOrDistanceChanged,
setOnRouteChanged,
setOnTurnByTurn,
} = useNavigation();

const [detoxStepNumber, setDetoxStepNumber] = useState(0);
Expand Down Expand Up @@ -229,6 +231,7 @@ const IntegrationTestsScreen = () => {
setOnRemainingTimeOrDistanceChanged,
setOnRouteChanged,
setOnLocationChanged,
setOnTurnByTurn,
passTest,
failTest,
setDetoxStep,
Expand Down Expand Up @@ -321,6 +324,9 @@ const IntegrationTestsScreen = () => {
case 'testSetFollowingPerspective':
await testSetFollowingPerspective(getTestTools());
break;
case 'testNavInfoEventsAfterCleanup':
await testNavInfoEventsAfterCleanup(getTestTools());
break;
default:
resetTestState();
break;
Expand Down Expand Up @@ -550,6 +556,13 @@ const IntegrationTestsScreen = () => {
}}
testID="testSetFollowingPerspective"
/>
<ExampleAppButton
title="testNavInfoEventsAfterCleanup"
onPress={() => {
runTest('testNavInfoEventsAfterCleanup');
}}
testID="testNavInfoEventsAfterCleanup"
/>
</OverlayModal>
</View>
);
Expand Down
105 changes: 105 additions & 0 deletions example/src/screens/integration_tests/integration_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
type NavigationController,
type NavigationViewController,
type TimeAndDistance,
type TurnByTurnEvent,
} from '@googlemaps/react-native-navigation-sdk';
import { Platform } from 'react-native';
import { delay, roundDown } from './utils';
Expand All @@ -48,6 +49,9 @@ interface TestTools {
setOnLocationChanged: (
listener: ((location: Location) => void) | null | undefined
) => void;
setOnTurnByTurn: (
listener: ((turnByTurnEvents: TurnByTurnEvent[]) => void) | null | undefined
) => void;
passTest: () => void;
failTest: (message: string) => void;
setDetoxStep: (stepNumber: number) => void;
Expand Down Expand Up @@ -1866,3 +1870,104 @@ export const testSetFollowingPerspective = async (testTools: TestTools) => {

await initializeNavigation(navigationController, failTest);
};

/**
* Tests that navInfo (turn-by-turn) events can be received after performing
* a cleanup and re-initialization cycle. This verifies that the NavForwardingManager
* and LiveData observer are properly restored after cleanup.
*/
export const testNavInfoEventsAfterCleanup = async (testTools: TestTools) => {
const {
navigationController,
setOnNavigationReady,
setOnLocationChanged,
setOnTurnByTurn,
passTest,
failTest,
} = testTools;

// Accept ToS first
if (!(await acceptToS(navigationController, failTest))) {
return;
}

const startLocation: LatLng = {
lat: 37.79136614772824,
lng: -122.41565900473043,
};

const destination = {
title: 'Grace Cathedral',
position: {
lat: 37.791957,
lng: -122.412529,
},
};

let phase: 'first' | 'second' = 'first';

setOnTurnByTurn(async (_events: TurnByTurnEvent[]) => {
if (phase === 'first') {
// Received navInfo in first session — now cleanup and re-init
phase = 'second';
setOnTurnByTurn(null);

await navigationController.cleanup();

// Re-initialize after cleanup
setOnNavigationReady(async () => {
disableVoiceGuidanceForTests(navigationController);
navigationController.setTurnByTurnLoggingEnabled(true);

const located2 = await simulateAndWaitForLocation(
navigationController,
setOnLocationChanged,
startLocation
);
if (!located2) {
return failTest(
'Timed out waiting for simulated location after re-init'
);
}
await navigationController.setDestination(destination);
await navigationController.startGuidance();
await navigationController.simulator.simulateLocationsAlongExistingRoute(
{ speedMultiplier: 5 }
);

// Listen for turn-by-turn events in the second session
setOnTurnByTurn(async () => {
// Received navInfo after cleanup+reinit — test passes
setOnTurnByTurn(null);
await navigationController.cleanup();
passTest();
});
});

await initializeNavigation(navigationController, failTest);
}
});

setOnNavigationReady(async () => {
disableVoiceGuidanceForTests(navigationController);
navigationController.setTurnByTurnLoggingEnabled(true);

const located = await simulateAndWaitForLocation(
navigationController,
setOnLocationChanged,
startLocation
);
if (!located) {
return failTest(
'Timed out waiting for simulated location to be confirmed'
);
}
await navigationController.setDestination(destination);
await navigationController.startGuidance();
await navigationController.simulator.simulateLocationsAlongExistingRoute({
speedMultiplier: 5,
});
});

await initializeNavigation(navigationController, failTest);
};
5 changes: 5 additions & 0 deletions ios/react-native-navigation-sdk/NavModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,14 @@ - (void)initializeSession {
_session.started = YES;

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

[self->_session.roadSnappedLocationProvider removeListener:self];
[self->_session.roadSnappedLocationProvider addListener:self];

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

self.enableUpdateInfo = NO;
self->_session.started = NO;
self->_session = nil;

Expand Down
1 change: 1 addition & 0 deletions scripts/addlicense.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ addlicense -f header_template.txt $@ \
--ignore "coverage/**" \
--ignore ".yarn/**" \
--ignore ".github/ISSUE_TEMPLATE/**" \
--ignore ".github/java-upgrade/**" \
.
Loading