diff --git a/android/src/main/java/com/google/android/react/navsdk/NavForwardingManager.java b/android/src/main/java/com/google/android/react/navsdk/NavForwardingManager.java index 734069f..8b805b2 100644 --- a/android/src/main/java/com/google/android/react/navsdk/NavForwardingManager.java +++ b/android/src/main/java/com/google/android/react/navsdk/NavForwardingManager.java @@ -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) { diff --git a/android/src/main/java/com/google/android/react/navsdk/NavModule.java b/android/src/main/java/com/google/android/react/navsdk/NavModule.java index a0edea3..073277a 100644 --- a/android/src/main/java/com/google/android/react/navsdk/NavModule.java +++ b/android/src/main/java/com/google/android/react/navsdk/NavModule.java @@ -81,6 +81,7 @@ public class NavModule extends NativeNavModuleSpec private Navigator.TrafficUpdatedListener mTrafficUpdatedListener; private Navigator.ReroutingListener mReroutingListener; private Navigator.RemainingTimeOrDistanceChangedListener mRemainingTimeOrDistanceChangedListener; + private Observer mNavInfoObserver; private @Navigator.TaskRemovedBehavior int taskRemovedBehaviour = Navigator.TaskRemovedBehavior.CONTINUE_SERVICE; @@ -174,6 +175,7 @@ public void cleanup(final Promise promise) { } final Navigator navigator = mNavigator; + UiThreadUtil.runOnUiThread( () -> { // Remove listeners on UI thread to serialize with callback dispatch. @@ -181,8 +183,14 @@ public void cleanup(final Promise promise) { // 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); }); @@ -279,14 +287,15 @@ public void initializeNavigationSession( initializeNavigationApi(); // Observe live data for nav info updates. - Observer 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); } }); } @@ -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); } } @@ -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); diff --git a/example/e2e/navigation.test.js b/example/e2e/navigation.test.js index 30dc674..bcf4052 100644 --- a/example/e2e/navigation.test.js +++ b/example/e2e/navigation.test.js @@ -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(); + }); }); diff --git a/example/src/screens/IntegrationTestsScreen.tsx b/example/src/screens/IntegrationTestsScreen.tsx index 7540a76..292bd34 100644 --- a/example/src/screens/IntegrationTestsScreen.tsx +++ b/example/src/screens/IntegrationTestsScreen.tsx @@ -57,6 +57,7 @@ import { testMapStyle, testMinMaxZoomLevels, testSetFollowingPerspective, + testNavInfoEventsAfterCleanup, NO_ERRORS_DETECTED_LABEL, } from './integration_tests/integration_test'; @@ -91,6 +92,7 @@ const IntegrationTestsScreen = () => { setOnLocationChanged, setOnRemainingTimeOrDistanceChanged, setOnRouteChanged, + setOnTurnByTurn, } = useNavigation(); const [detoxStepNumber, setDetoxStepNumber] = useState(0); @@ -229,6 +231,7 @@ const IntegrationTestsScreen = () => { setOnRemainingTimeOrDistanceChanged, setOnRouteChanged, setOnLocationChanged, + setOnTurnByTurn, passTest, failTest, setDetoxStep, @@ -321,6 +324,9 @@ const IntegrationTestsScreen = () => { case 'testSetFollowingPerspective': await testSetFollowingPerspective(getTestTools()); break; + case 'testNavInfoEventsAfterCleanup': + await testNavInfoEventsAfterCleanup(getTestTools()); + break; default: resetTestState(); break; @@ -550,6 +556,13 @@ const IntegrationTestsScreen = () => { }} testID="testSetFollowingPerspective" /> + { + runTest('testNavInfoEventsAfterCleanup'); + }} + testID="testNavInfoEventsAfterCleanup" + /> ); diff --git a/example/src/screens/integration_tests/integration_test.ts b/example/src/screens/integration_tests/integration_test.ts index c83dd29..4987e77 100644 --- a/example/src/screens/integration_tests/integration_test.ts +++ b/example/src/screens/integration_tests/integration_test.ts @@ -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'; @@ -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; @@ -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); +}; diff --git a/ios/react-native-navigation-sdk/NavModule.mm b/ios/react-native-navigation-sdk/NavModule.mm index 39e6cee..7ce3489 100644 --- a/ios/react-native-navigation-sdk/NavModule.mm +++ b/ios/react-native-navigation-sdk/NavModule.mm @@ -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]; @@ -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; diff --git a/scripts/addlicense.sh b/scripts/addlicense.sh index b7bc1af..9801eec 100755 --- a/scripts/addlicense.sh +++ b/scripts/addlicense.sh @@ -26,4 +26,5 @@ addlicense -f header_template.txt $@ \ --ignore "coverage/**" \ --ignore ".yarn/**" \ --ignore ".github/ISSUE_TEMPLATE/**" \ + --ignore ".github/java-upgrade/**" \ .