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
46 changes: 46 additions & 0 deletions MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,59 @@ This document covers breaking changes and migration steps between major versions

## Table of Contents

- [Migrating from 0.19.0 to 0.19.1](#migrating-from-0190-to-0191)
- [Migrating from 0.18.x to 0.19.x](#migrating-from-018x-to-019x)
- [Migrating from 0.17.x to 0.18.x](#migrating-from-017x-to-018x)
- [Migrating from 0.16.x to 0.17.x](#migrating-from-016x-to-017x)
- [Migrating from 0.13.x to 0.14.x](#migrating-from-013x-to-014x)

---

## Migrating from 0.19.0 to 0.19.1

Version 0.19.1 upgrades the Android Navigation SDK from `8.0.0` to
`8.1.0`. The iOS Navigation SDK remains at `11.3.0`.

### Migrate route tokens

`SetDestinationsOptions.routeTokenOptions` and `RouteTokenOptions` are
deprecated and still work. Move token routing to `setDestinationsWithToken`.

Before:

```tsx
await navigationController.setDestinations(waypoints, {
routeTokenOptions: { routeToken, travelMode: TravelMode.DRIVING },
displayOptions: { showDestinationMarkers: false },
});
```

After:

```tsx
await navigationController.setDestinationsWithToken({
routeToken,
travelMode: TravelMode.DRIVING,
displayOptions: { showDestinationMarkers: false },
});
```

- Drop the waypoints and `routingOptions`: the token supplies the destinations
and routing preferences.
- Pass the travel mode the token was generated with. Modes other than
`TravelMode.DRIVING` and `TravelMode.TWO_WHEELER` reject with error code
`travelModeUnsupported`.
- Destination markers are shown when `displayOptions` is omitted.

See [Using Route Tokens](README.md#using-route-tokens) for the full behavior,
including the platform difference when `travelMode` is omitted.

### Rebuild both platforms

Rebuild Android, then run `pod install` in your iOS directory and rebuild iOS.

---

## Migrating from 0.18.x to 0.19.x

Version 0.19.0 upgrades the Android Navigation SDK from `7.9.0` to `8.0.0` and
Expand Down
38 changes: 19 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ package's config plugin instead of the Android and iOS steps below. See the

### Android

This package uses Navigation SDK for Android **8.0.0**. See the
This package uses Navigation SDK for Android **8.1.0**. See the
[0.19.x migration guide](./MIGRATING.md#migrating-from-018x-to-019x)
for build requirements and breaking API changes.

Expand Down Expand Up @@ -337,30 +337,30 @@ try {

#### Using Route Tokens

You can use a pre-computed route from the [Routes API](https://developers.google.com/maps/documentation/routes) by providing a route token. This is useful when you want to ensure the navigation follows a specific route that was calculated server-side.
Use a route token from the [Routes API](https://developers.google.com/maps/documentation/routes/route_token) or [Route Optimization API](https://developers.google.com/maps/documentation/route-optimization/polylines-and-route-tokens) to navigate a route planned by your backend.

To use a route token:

1. Pass the token using `routeTokenOptions` instead of `routingOptions`
2. **Important:** The waypoints passed to `setDestinations` must match the waypoints used when generating the route token
After navigation initialization and a valid location update, pass a `SetDestinationsWithTokenOptions` object:

```tsx
const waypoint = {
title: 'Destination',
position: { lat: 37.7749, lng: -122.4194 },
};
const routeStatus = await navigationController.setDestinationsWithToken({
routeToken,
travelMode: TravelMode.DRIVING, // Use the mode the token was generated with.
displayOptions: { showDestinationMarkers: true }, // Optional; defaults to true.
});
if (routeStatus === RouteStatus.OK) {
await navigationController.startGuidance();
}
```

const routeTokenOptions = {
routeToken: 'your-route-token-from-routes-api',
travelMode: TravelMode.DRIVING, // Must match the travel mode used to generate the token
};
The required `routeToken` supplies the destinations and routing preferences; do not pass waypoints or `routingOptions`. The optional nested `displayOptions` defaults to showing destination markers. Guidance, simulation, arrival events, and `continueToNextDestination` work with token routes as they do with waypoint routes. Arrival waypoints are reported by the native SDK, not matched against caller-provided labels.

await navigationController.setDestinations([waypoint], { routeTokenOptions });
await navigationController.startGuidance();
```
The optional `travelMode` accepts only `TravelMode.DRIVING` or `TravelMode.TWO_WHEELER`; pass the mode the token was generated with. Explicit unsupported modes (including walking, cycling, and taxi) reject with native error code `travelModeUnsupported` before changing the existing route. On iOS, omitting `travelMode` preserves the current client mode and rejects with the same code if that mode is unsupported; pass an explicit supported mode to switch after walking or cycling. On Android, omission follows the native token-routing behavior and uses the token's mode. A supported client mode never overrides the travel mode encoded in the token.

> [!IMPORTANT]
> `routingOptions` and `routeTokenOptions` are mutually exclusive. Providing both will throw an error.
For Routes API tokens, use `DRIVE` or `TWO_WHEELER`, a `TRAFFIC_AWARE` or `TRAFFIC_AWARE_OPTIMAL` routing preference, and no `via` waypoints.

`RouteStatus.OK` means the SDK found a route, not that it reproduced the planned route exactly. Starting location, traffic, or road changes can affect the route. This API returns `RouteStatus`, not token execution details.

`SetDestinationsOptions.routeTokenOptions` and `RouteTokenOptions` are deprecated. Existing `setDestination` and `setDestinations` callers can migrate using the [token-routing migration guide](./MIGRATING.md#migrating-from-0190-to-0191). In the deprecated API, `routingOptions` and `routeTokenOptions` remain mutually exclusive.


#### Adding navigation listeners
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@ dependencies {
implementation "androidx.car.app:app:1.4.0"
implementation "androidx.car.app:app-projected:1.4.0"
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "com.google.android.libraries.navigation:navigation:8.0.0"
implementation "com.google.android.libraries.navigation:navigation:8.1.0"
api 'com.google.guava:guava:31.0.1-android'
}
7 changes: 4 additions & 3 deletions android/consumer-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
public static *;
}

# SDK 8's protobuf registry is package-private. Its reflective loader must stay
# in the same package; R8 repackaging otherwise causes IllegalAccessException.
-keep class com.google.android.libraries.navigation.internal.amv.av { *; }
# SDK 8.1's protobuf registry (internal.anr.k) is package-private. Its reflective
# loader must stay in the same package; R8 repackaging otherwise causes
# IllegalAccessException even when optimizations are disabled.
-keep class com.google.android.libraries.navigation.internal.anr.av { *; }
Comment thread
jokerttu marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,49 @@ public void setDestinations(
}
}

@Override
public void setDestinationsWithToken(ReadableMap options, final Promise promise) {
if (!ensureNavigatorAvailable(promise)) {
return;
}

try {
if (options.hasKey("travelMode") && !options.isNull("travelMode")) {
Comment thread
jokerttu marked this conversation as resolved.
Resolved
@RoutingOptions.TravelMode
int travelMode = EnumTranslationUtil.getTravelModeFromJsValue(options.getInt("travelMode"));
if (travelMode != RoutingOptions.TravelMode.DRIVING
&& travelMode != RoutingOptions.TravelMode.TWO_WHEELER) {
promise.reject(
"travelModeUnsupported",
"Route tokens support only DRIVING and TWO_WHEELER travel modes.");
return;
}
}

String routeToken = options.getString("routeToken");
DisplayOptions parsedDisplayOptions =
ObjectTranslationUtil.getDisplayOptionsFromMap(
options.getMap("displayOptions").toHashMap());

mWaypoints.clear();

// The SDK uses the mode encoded in the token, not the validated client mode.
mNavigator.setDestinationsWithToken(
routeToken,
parsedDisplayOptions,
routeDescription -> {
if (routeDescription == null) {
promise.reject("noRouteStatus", "The Navigation SDK returned no route status.");
return;
}
promise.resolve(
EnumTranslationUtil.getRouteStatusStringValue(routeDescription.getRouteStatus()));
});
} catch (RuntimeException e) {
promise.reject("setDestinationsWithTokenFailed", e.getMessage(), e);
}
}

@Override
public void clearDestinations(final Promise promise) {
if (!ensureNavigatorAvailable(promise)) {
Expand Down Expand Up @@ -702,7 +745,7 @@ public void startGuidance(final Promise promise) {
if (!ensureNavigatorAvailable(promise)) {
return;
}
if (mWaypoints.isEmpty()) {
if (mNavigator.getCurrentRouteSegment() == null) {
promise.reject(JsErrors.NO_DESTINATIONS_ERROR_CODE, JsErrors.NO_DESTINATIONS_ERROR_MESSAGE);
return;
}
Expand All @@ -723,15 +766,17 @@ public void stopGuidance(final Promise promise) {

@Override
public void simulateLocationsAlongExistingRoute(ReadableMap options, final Promise promise) {
float speedMultiplier =
options.hasKey("speedMultiplier") ? (float) options.getDouble("speedMultiplier") : 1.0f;
if (mNavigator == null) {
if (!ensureNavigatorAvailable(promise)) {
return;
}
if (mWaypoints.isEmpty()) {
if (mNavigator.getCurrentRouteSegment() == null) {
promise.reject(JsErrors.NO_DESTINATIONS_ERROR_CODE, JsErrors.NO_DESTINATIONS_ERROR_MESSAGE);
return;
}

float speedMultiplier =
options.hasKey("speedMultiplier") ? (float) options.getDouble("speedMultiplier") : 1.0f;

mNavigator
.getSimulator()
.simulateLocationsAlongExistingRoute(
Expand Down
34 changes: 34 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,37 @@ First, make sure you go through the setup from the main [README](../README.md).
1. On your Emulator, go to App Info for the installed app, then Permissions > Location and allow location for the app.

2. Restart the app, now the Navigation view should be displayed instead of the map.

## Route-token integration tests

The focused route-token suite requests fresh single- and multi-stop tokens from
the Routes API, then exercises native routing, omitted and supported client modes,
nested display options, guidance, simulation, arrival, route replacement, and the
deprecated API on both platforms. Configure the native Navigation SDK keys above
and follow the [Detox setup](../CONTRIBUTING.md#integration-tests).

Enable Routes API for a test key that permits requests from the test machine.
An Android- or iOS-app-restricted key may not permit these Node-side requests.
The key stays in the test process; only the resulting token is entered into
the app. Do not commit keys or route tokens.

From the repository root:

```sh
yarn example detox:build:android-release
yarn example detox:build:ios-release

# Paste the Routes API key, then press Enter; input is not echoed.
read -r -s ROUTES_API_KEY
export ROUTES_API_KEY

yarn example detox:test:android-release --testMatch "$PWD/example/e2e/route_token.e2e.js"
yarn example detox:test:ios-release --testMatch "$PWD/example/e2e/route_token.e2e.js" --headless

unset ROUTES_API_KEY
```

The default `*.test.js` suite does not require this additional key. Its token
validation coverage checks unsupported mode rejection and prior-route preservation
using an invalid token. The focused suite fails explicitly if `ROUTES_API_KEY` is
missing or cannot obtain a token.
2 changes: 1 addition & 1 deletion example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ dependencies {
implementation "androidx.car.app:app-projected:1.4.0"

// Include the Google Navigation SDK.
implementation 'com.google.android.libraries.navigation:navigation:8.0.0'
implementation 'com.google.android.libraries.navigation:navigation:8.1.0'

// 1.2.1 pads libdatastore_shared_counter.so's GNU_RELRO segment for 16 KB pages.
implementation 'androidx.datastore:datastore-core:1.3.0'
Expand Down
2 changes: 1 addition & 1 deletion example/e2e/navigation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('Navigation tests', () => {
await expectSuccess();
});

it('NT08 - setDestinations with both routingOptions and routeTokenOptions should throw error', async () => {
it('NT08 - rejects deprecated routeTokenOptions combined with routingOptions and unsupported token modes without changing route state', async () => {
await selectTestByName('testRouteTokenOptionsValidation');
await agreeToTermsAndConditions();
await waitForTestToFinish();
Expand Down
120 changes: 120 additions & 0 deletions example/e2e/route_token.e2e.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { device, element, waitFor, by } from 'detox';
import {
initializeIntegrationTestsPage,
agreeToTermsAndConditions,
selectTestByName,
waitForTestToFinish,
expectSuccess,
expectNoErrors,
} from './shared.js';

// Opt in with --testMatch and this file's absolute path; see example/README.md.
// The default suite needs no Routes API key; targeted runs require one.
// Keep these fixtures in sync with the in-app token integration scenarios.
const ORIGIN = { latitude: 37.791366, longitude: -122.415659 };
const DESTINATION = { latitude: 37.791957, longitude: -122.412529 };
const INTERMEDIATE = { latitude: 37.79165, longitude: -122.4141 };

const fetchFreshRouteToken = async (multiStop = false) => {
const apiKey = process.env.ROUTES_API_KEY?.trim();
if (!apiKey) {
throw new Error(
'ROUTES_API_KEY is required for the targeted token-routing tests; enable Routes API for this credential.'
);
}

let response;
try {
response = await fetch(
'https://routes.googleapis.com/directions/v2:computeRoutes',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Goog-Api-Key': apiKey,
'X-Goog-FieldMask': 'routes.routeToken',
},
body: JSON.stringify({
origin: { location: { latLng: ORIGIN } },
destination: { location: { latLng: DESTINATION } },
intermediates: multiStop
? [{ location: { latLng: INTERMEDIATE }, via: false }]
: [],
travelMode: 'DRIVE',
routingPreference: 'TRAFFIC_AWARE',
}),
signal: AbortSignal.timeout(30000),
}
);
} catch (error) {
// Do not include request headers, credentials, or an unsanitized HTTP error.
const code = error?.cause?.code ?? error?.code ?? error?.name;
throw new Error(
`Routes API request failed before an HTTP response (${code}).`
);
}
if (!response.ok) {
throw new Error(`Routes API request failed (HTTP ${response.status}).`);
}

let data;
try {
data = await response.json();
} catch {
throw new Error(
`Routes API returned invalid JSON (HTTP ${response.status}).`
);
}
const token = data.routes?.[0]?.routeToken;
if (typeof token !== 'string' || !token.trim()) {
throw new Error('Routes API returned no route token.');
}
return token;
};

const runTokenTest = async (testName, multiStop = false) => {
// Fetch immediately before each run; do not hardcode or cache route tokens.
const routeToken = await fetchFreshRouteToken(multiStop);
await initializeIntegrationTestsPage();
await device.disableSynchronization();
const tokenInput = element(by.id('route_token_input'));
await waitFor(tokenInput).toBeVisible().withTimeout(10000);
await tokenInput.tap();
await tokenInput.replaceText(routeToken);
if (device.getPlatform() === 'android') {
await device.pressBack();
} else {
await tokenInput.tapReturnKey();
}
await selectTestByName(testName);
await agreeToTermsAndConditions();
await waitForTestToFinish(180000);
await expectNoErrors();
await expectSuccess();
};

describe('Fresh route-token navigation tests', () => {
it('NT12 - handles omitted and supported client modes, replaces routes, routes with deprecated routeTokenOptions, clears, guides and arrives', async () => {
await runTokenTest('testTokenRouting');
}, 240000);

it('NT13 - follows a multi-stop token, continues between SDK legs and clears the final route', async () => {
await runTokenTest('testMultiStopTokenRouting', true);
}, 240000);
});
Loading
Loading