Android Navigation SDK

Android NavSDK Screenshots

The MapQuest Navigation SDK enables turn-by-turn GPS in any iOS or Android application. Developers now have the ability to customize their UI and gain insight into the navigation experience as users drive. This SDK does not generate transactions on your account and is available free to use.

Navigation SDK is only available in the U.S. at this time. If you would like to use Navigation SDK outside of the U.S. please contact your account manager or customer support

Features

  • A Sample Navigation App provided as a reference design for customers, and to show off best UX practices for a navigation experience.
  • Improved Destination Requests support MapQuest POI identifiers for improved routing.New in 3.2.0
  • Multi-Stop Routes allow intermediate waypoints for a route -- with pause/resume functionality.New in 3.2
  • Language support for both US English and US Spanish.New in 3.2
  • Destination Arrival listener gives you control over when navigation ends or moves to the next route-leg.New in 3.2
  • Traffic-influenced routes based on current traffic conditions.
  • Traffic-influenced reroutes return automatically when a faster route is available.
  • Route tracking with off route detection and automatic rerouting to destination.
  • Turn-by-turn voice guidance, including robust highway sign information.
  • Pluggable text-to-speech (TTS) system allows developers to choose their own TTS system to enable custom voices or other system-wide behavior. We provide a default TTS system to work on any device.
  • Directions list with simple, easy-to-read turn instructions.
  • Dynamic ETA based on current traffic conditions as a driver travels along the route.
  • Speed limits along the route.
  • Traffic Data Collection is enabled by default, but can be disabled; ask your MapQuest Account Representative for details.New in 3.3

Complementary APIs

The following APIs are often used with the Navigation SDK, but note these do incur a transaction cost to use:

  • Maps SDK : used for the MapView as demonstrated in the Sample Application; further documentation is available here
  • Geocoding APIs : full documentation available here
  • Search / Search Ahead APIs : full documentation available here

Getting Started

Project Setup and Configuration

  • Create a new Android project; note that the minimum Android SDK version supported by the MapQuest NavSDK is API level 16.

  • Add the following to the top of your app’s build.gradle file -- such that it includes the maven URL for the MapQuest artifacts repository:

    allprojects {
        repositories {
            jcenter()
            maven {
                url "https://maven.google.com"
            }
            maven {
                url "http://artifactory.cloud.mapquest.com/artifactory/et-android-binaries"
            }
        }
    }
  • Also add the following items under the dependencies section of the build.gradle file for the app:

    dependencies {
        compile('com.mapquest:navigation:3.3.0')            // the MapQuest Navigation SDK
        compile('com.mapzen.android:lost:3.0.4')            // used for GPS Location Acquisition (see notes)
    
        compile('com.mapquest:mapping-android-sdk:2.0.1') { // for Mapping SDK MapView; displays route to navigate
            exclude group: 'com.android.support'            // (note: in your app, you might omit this 'exclude')
        }
    
        compile('com.mapquest:searchahead:1.3.0)      // for the MapQuest "Search Ahead" feature (optional)
    }

MapQuest Key

In order to use MapQuest APIs and SDKs you will need a MapQuest key. We use this key to associate your requests to APIs with your account. You can find your existing keys or create a new one on the Applications page.

Sample Key Page

If you don't have a MapQuest Developer Account you can Sign Up Here.

Using your MapQuest Key in the SDK

Once you have a MapQuest Key, per above, create a new file named mapquest.properties in the app directory of your project, containing the following single-line entry:

api_key=[PUT_YOUR_API_KEY_HERE]

Then, add the following items under the android section of the build.gradle file for the app:

applicationVariants.all { variant ->
     variant.buildConfigField "String", "API_KEY", getApiKey() // Provides key for the Navigation SDK
     variant.resValue "string", "API_KEY", getApiKey()         // Provides key for the MapView used in app layouts
 }

Per the above, also add the following method (at the top-level) to the build.gradle for the app -- to define the getApiKey() method:

def getApiKey() {
    def props = new Properties()
    file("mapquest.properties").withInputStream { props.load(it) }
    return "\"" + props.getProperty("api_key") + "\""
}

Terminology

  • route: a complete trip from a start location through one or more destinations. For example, a trip starting from a user’s home, stopping at the coffee shop, and then to work is a route.
  • destination: either the final destination of a route, per above; or, an intermediate destination along the route.
  • leg: the part of a route between two destinations. You can think of a route as a sequence of legs joined together end-to-end. The route above has two legs, from home to the coffee shop, and from the coffee shop to work.
  • maneuver: an action called out by the NavSDK informing that the user must make a turn, exit a highway, etc.
  • prompt: spoken narration as the user drives. Examples: “In half a mile, turn right onto Main Street.”
  • position: The SDK uses a common coordinate system for anything that references a point along a route. Anything that exists along the route line has a position. Maneuvers, prompts, and the user’s current progress along the route are all expressed using positions. A position is a floating point number indexing into the route line’s coordinate array. A maneuver with position of 3 is located at the 4th coordinate in the route’s shape. A prompt with position 23.5 is placed halfway between the coordinates at index 23 and 24. This system provides a continuous reference of points along the route.
  • position span: The SDK uses spans to represent features that exist on long sections of the route, such as traffic information and speed limits. The SDK provides helpers to compute lengths, distances, and shapes of positions and position spans.
  • snapped location: While navigating, the NavSDK tracks the user as they progress along the route. With each GPS update, the NavSDK determines if the user is “on route” or whether they have deviated from the desired route. When “on route” the NavSDK computes a snapped location, which represent a position along the route, including a coordinate, bearing, and other properties that reflect a point exactly along the route line. If the user is “off route”, then the NavSDK will not provide a snapped location.

Simple Example Usage

Before adding any of the example code outlined below, try building your app thus far -- to ensure that the above setup and library dependencies are indeed correct.

Note that in addition to the implementation notes provided in each section below, it will often prove helpful to refer to the MapQuest Navigation SDK Reference Sample Application, which provides a complete working example upon which you can base your own host application.

In general, developers using the Navigation SDK will first create a query against the RouteService, and then call the method startNavigation on the returned route -- using an instance of the NavigationManager which captures callbacks for navigation-related events. Events are triggered when:

  • The user passes a maneuver and the current step should be updated
  • The user has deviated from the route
  • A new reroute is received
  • A new ETA is received
  • New traffic data is received
  • A traffic-based reroute is received from the server
  • A navigation prompt should be spoken to the user
  • The speed limit changed
  • A new location is received
  • Navigation has been paused or resumed
  • A destination has been reached

Here's a simple example of how to create a route from New York City to Boston, and then start a turn-by-turn navigation session. First, we create an instance of the RouteService, using our API_KEY, like so:

mRouteService = new RouteService.Builder().build(getApplicationContext(), BuildConfig.API_KEY);

Additionally, we'll also need an instance of NavigationManager, which we will use to navigate the route selected after it has been retrieved -- note that it requires a LocationProviderAdapter (discussed further below):

mNavigationManager = new NavigationManager.Builder(this, BuildConfig.API_KEY)
                .withLocationProviderAdapter(mApp.getLocationProviderAdapter())
                .build();

Now, we can define a start and destination(s) for the route -- and query the RouteService for the possible route(s) between these locations -- specifying various RouteOptions as desired -- for example to allow (or disallow) highway routes, toll-roads, ferries, etc.

Note also that language can now also be specified as an option; specify the IETF language tag (with locale) as the string argument, e.g. "en_US" for English in the US; or "es_US" for Spanish in the US.New in 3.2

// Set up start and destination for the route
Coordinate nyc = new Coordinate(40.7326808, -73.9843407);
List<Coordinate> boston = Arrays.asList(new Coordinate(42.355097, -71.055464));

// Set up route options
RouteOptions routeOptions = new RouteOptions.Builder()
        .maxRoutes(3)
        .systemOfMeasurementForDisplayText(SystemOfMeasurement.UNITED_STATES_CUSTOMARY) // or specify METRIC
        .language("en_US") // NOTE: alternately, specify "es_US" for Spanish in the US
        .highways(RouteOptionType.ALLOW)
        .tolls(RouteOptionType.ALLOW)
        .ferries(RouteOptionType.DISALLOW)
        .internationalBorders(RouteOptionType.DISALLOW)
        .unpaved(RouteOptionType.DISALLOW)
        .seasonalClosures(RouteOptionType.AVOID)
        .build();

mRouteService.requestRoutes(nyc, boston, routeOptions, new RoutesResponseListener() {
    @Override
    public void onRoutesRetrieved(List<Route> routes) {
        if (routes.size() > 0) {
            mNavigationManager.startNavigation((Route) routes.get(0));
        }
    }

    @Override
    public void onRequestFailed(@Nullable Integer httpStatusCode, @Nullable IOException exception) {}

    @Override
    public void onRequestMade() {}
});

In the example onRoutesRetrieved callback, above, note that we use our NavigationManager to startNavigation on, say, the first Route that was retrieved -- though in a real application we might first "render" the resulting routes on a map-view, and allow the user to select the one they wish to navigate.

Also, note that the system of measurement used for any prompts or other "display text" in the returned routes can also be specified using systemOfMeasurementForDisplayText; which can be set to either UNITED_STATES_CUSTOMARY or METRIC. However, do note that all distance values in the route data itself are always returned in meters (metric units); i.e. this option affects the units for text values only.

Refer to the Navigation SDK Reference Sample Application code to see a complete example of how to leverage the MapQuest Android SDK (MapView) to draw the routes returned from the RouteService.

Next Steps: Leveraging Navigation Callbacks

Now that you have the basics in place -- using the RouteService and using the NavigationManager to start navigation along a selected route -- the next step is to leverage one or more of the available callbacks provided by various listener interfaces, provided in the SDK package: com.mapquest.navigation.listener.

Simply define an implementation of a given listener interface, and implement the desired functionality for each callback method of interest. For example, if we want to simply update a message in the UI to inform the user that navigation has started or stopped, we can implement a NavigationStartStopListener, and add it to our NavigationManager instance, like so:

mNavigationManager.addAndNotifyNavigationStartStopListener(new NavigationStartStopListener() {
    @Override
    public void onNavigationStarted() {
        Toast.makeText(mApp, "Navigation Started...", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onNavigationStopped(@NonNull RouteStoppedReason routeStoppedReason) {
        Toast.makeText(mApp, "Navigation Stopped.", Toast.LENGTH_SHORT).show();
    }
});

Another commonly used listener is the EtaResponseListener, used to update the UI when the Estimated Time of Arrival has changed while navigating a route. For example, you could add one like so:

mNavigationManager.addEtaResponseListener(new EtaResponseListener() {
    @Override
    public void onEtaUpdate(@NonNull EstimatedTimeOfArrival estimatedTimeOfArrival) {
        // TODO: update your ETA (text) view here...
    }
});

The complete set of available listeners are described in detail in the Navigation SDK API Docs, and include:

  • NavigationProgressListener: called when the user's progress along the route has been updated, and when the user reaches one of the destinations on their route; whereby the host application can update, say, a current location indicator -- and determine how to proceed after reaching an intermediate destination;
  • PromptListener: called when navigation guidance "prompts" should be spoken to the user -- using whatever TTS engine you wish;
  • SpeedLimitSpanListener: called when speed-limit boundaries are crossed, so the host application can update displayed speed-limit information;
  • RerouteResponseListener: called when the user goes "off-route", and needs to be re-routed -- so your application can request a new route, as appropriate;
  • NavigationStartStopListener: called when navigation has either started or stopped;
  • EtaResponseListener: called when the estimated time of arrival has changed.

Destinations and Multi-Stop

Upon reaching a destination, the NavigationProgressListener provides an method which notifies this listener of the RouteLeg that was completed, and whether the destination was the final destination.

If you implement this listener method, you are required to call the DestinationAcceptanceHandler provided in order to either continue to the next destination or finish navigation.New in 3.2

Note that by default, navigation automatically stops when the final destination has been reached. For intermediate destinations, you may want to effectively pause navigation; and provide some kind of user-interface (e.g. an alert-dialog) to allow the user to resume navigating once they continue driving towards the next route-stop.

Refer to the sample app for an example implementation of how to handle the onDestinationReached callback; here's the relevant snippet, as supplied in the example NavigationActivity:

@Override
public void onDestinationReached(@NonNull Destination destination, boolean isFinalDestination,
                                 @NonNull RouteLeg routeLegCompleted,
                                 @NonNull final DestinationAcceptanceHandler destinationAcceptanceHandler) {

    if (!isFinalDestination) {
        final AlertDialog alertDialog = new AlertDialog.Builder(NavigationActivity.this)
                .setTitle("Arrived at Waypoint")
                .setMessage("Intermediate destination reached. Press 'Proceed' to proceed to the next stop...")
                .setPositiveButton("Proceed", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        destinationAcceptanceHandler.confirmArrival(true);
                        updateDirectionsList();

                        // allow user to skip ahead only if not on final leg of route
                        showSkipLegButton(!isNavigatingFinalRouteLeg());
                    }
                })
                .setOnCancelListener(new DialogInterface.OnCancelListener() {
                    @Override
                    public void onCancel(DialogInterface dialog) {
                        destinationAcceptanceHandler.confirmArrival(false);
                    }
                })
                .show();

    } else {
        clearMarkup();

        final AlertDialog alertDialog = new AlertDialog.Builder(NavigationActivity.this)
                .setTitle("Done Navigating")
                .setMessage("Final destination reached")
                .setPositiveButton("OK", new OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        destinationAcceptanceHandler.confirmArrival(true);
                        finish(); // OK, done with NavigationActivity...
                    }
                }).setOnCancelListener(new DialogInterface.OnCancelListener() {
                    @Override
                    public void onCancel(DialogInterface dialog) {
                        destinationAcceptanceHandler.confirmArrival(false);
                    }
                })
                .show();
    }
}

Note that your host application can further inspect the Destination object passed to the above handler to provide the user more details about the waypoint or destination reached; especially if the destination was specified as an MQID.

Location Services: How to Provide Accurate Location Updates to the Navigation Manager

When navigating a route using the NavigationManager, the user's location updates are provided to the SDK by a location provider. The abstract class LocationProviderAdapter in the SDK provides a generic "wrapper" for any location-provider of your choice -- for example, a native Google location service, a "mocked" location-provider (e.g. used during testing), or one of the commonly used 3rd-party location libraries such as Mapzen Lost.

The Navigation SDK Reference Sample Application provides an example implementation using the Mapzen Lost location provider library, MapzenLocationProviderAdapter -- refer to the documentation for more details on the LocationProviderAdapter and the implementation of MapzenLocationProviderAdapter.

Using a Foreground (Notification) Service to Ensure Location Updates

Normally, the Navigation SDK can be used only while the application is in the foreground. However, if developers want to have the navigation experience continue while the app has been backgrounded -- and also to persist through various Android OS actions (e.g. if the app being killed to free memory, or per Android Oreo location-update restrictions), some extra setup is required. To handle this use-case correctly, a Foreground Service should be created, started, and bound to your application.

In the sample app provided, NavigationNotificationService is an example Android Service which maintains an instance of NavigationManager. It is used to ensure navigation continues in the case that the app is backgrounded and reclaimed by the OS. Activities in the sample app can bind to the service and retrieve the navigation manager.

In addition, note that the example NavigationNotificationService is also responsible for building and updating various notifications that occur during navigation -- which appear, of course, in the system Notification Drawer -- and which are typically shown from an application running in the background.

Navigation Prompts: Text to Speech Implementation

Note that the MapQuest Navigation SDK is not shipped with an integrated TTS (text-to-speech) implementation, but instead allows the developer to specify their own TTS engine per their application’s specific requirements.

This is accomplished by means of a simple interface, PromptListener, which can be added to the NavigationManager to handle each prompt event.

Nevertheless, an example implementation of TTS is provided as part of the sample app, which uses Android’s native TextToSpeech Engine -- see the TextToSpeechPromptListener class. This class delegates to the example TextToSpeechManager which handles all aspects of TTS playback, including “overlapping” prompt-events, managing the “audio focus”, and Android API backwards-compatibility concerns.

Traffic Data Collection: Requesting User Consent

The MapQuest Navigation SDK is designed to collect location data during active navigation sessions in order to improve the quality of our routes and traffic data. Verizon/Mapquest believes the user's privacy is of the highest importance. In order to safeguard user privacy, your application is required to request explicit consent from the user before navigation will proceed. There is currently no approved use case whereby Verizon data collected by this SDK may be shared with a third party.

Note that Traffic Data Collection is enabled by default in the MapQuest Navigation SDK, as of version 3.3. If the particulars of your application require that user location information is not used by MapQuest to improve its traffic and navigation services, you should inquire with your MapQuest Account Representative for details on how to disable this functionality.New in 3.3

In any case, your host application -- when using v3.3 or later of the MapQuest Navigation SDK -- must now present the user with a "Location Tracking Consent" dialog, and must call the method setUserLocationTrackingConsentStatus before it will be able to start navigating any route using the NavigationManager.

This status flag has three possible values, as defined in the enum class UserLocationTrackingConsentStatus:

  • AWAITING - this is the default state. The NavigationManager will not start navigation in this state.
  • GRANTED - the user has granted consent and NavigationManager will collect location information during active navigation only.
  • DENIED - the user has denied consent and NavigationManager will not collect location information.

Per the above, if you attempt to call startNavigation while the NavigationManager is in the awaiting consent state, navigation will not start and you will receive an error that consent has not been set. Therefore, before the very first navigation session, you must present the user with a consent dialog, and set this flag accordingly. You may, of course, persist the user's response and set it for future navigation sessions; i.e. the user need only agree to the terms once.

Our recommended in-app disclosure language for the consent dialog is as follows:

Allow collection of location information even when the app is not in use. In addition to use for in-app features, location helps improve other products and services provided by the Verizon family of companies.

Here are some example code-snippets from the sample app, which demonstrate how you might display such a dialog, set the user-consent flag, and pass it along (through a Navigation Activity) to the foreground-service -- where the NavigationManager is initialized. For example, here's a method which might be invoked when the user presses the "Start" button to begin navigating:

@OnClick(R.id.start)
protected void startNavigationActivity() {
    final SharedPreferences sharedPreferences = getSharedPreferences(SHARED_PREFERENCE_NAME, MODE_PRIVATE);
    if (sharedPreferences.contains(USER_TRACKING_CONSENT_KEY)) {
        //
        // user has already specified their consent (or not) to location tracking,
        // so start navigating the currently selected route...
        //
        startNavigation(mSelectedRoute, sharedPreferences.getBoolean(USER_TRACKING_CONSENT_KEY, false));

    } else {
        //
        // present the user with a consent dialog per location tracking, and persist this decision
        //
        new AlertDialog.Builder(this)
              .setTitle(R.string.user_tracking_consent_dialog_title)
              .setMessage(R.string.user_tracking_consent_dialog_message)
              .setCancelable(false)
              .setPositiveButton(R.string.user_tracking_consent_dialog_positive_button_text,
                      new DialogInterface.OnClickListener() {
                  @Override
                  public void onClick(DialogInterface dialog, int which) {
                      sharedPreferences.edit()
                              .putBoolean(USER_TRACKING_CONSENT_KEY, true)
                              .apply();
                      startNavigation(mSelectedRoute, true);
                  }
              })
              .setNegativeButton(R.string.user_tracking_consent_dialog_negative_button_text,
                      new DialogInterface.OnClickListener() {
                  @Override
                  public void onClick(DialogInterface dialog, int which) {
                      sharedPreferences.edit()
                              .putBoolean(USER_TRACKING_CONSENT_KEY, false)
                              .apply();
                      startNavigation(mSelectedRoute, false);
                  }
              })
              .show();
    }
}

...where the implementation of the method startNavigation used above would simply invoke your host application's NavigationActivity with the selected route, and with the boolean value expressing the user's consent decision.

For example:

private void startNavigation(Route route, boolean userAllowedTracking) {
    NavigationActivity.start(getApplicationContext(), route, userAllowedTracking);
}

Then, in this example, via means of an intent extra, the NavigationActivity passes the user-consent value along to the bound foreground-service, NavigationNotificationService -- which manages the NavigationManager instance -- and therefore is ultimately responsible for setting the consent status with this value via setUserLocationTrackingConsentStatus, like so:

private NavigationManager createNavigationManager(LocationProviderAdapter locationProviderAdapter) {
    mNavigationManager = new NavigationManager.Builder(this, BuildConfig.API_KEY, locationProviderAdapter).build();

    UserLocationTrackingConsentStatus userLocationTrackingConsentStatus = mUserConsentGranted ?
            UserLocationTrackingConsentStatus.GRANTED : UserLocationTrackingConsentStatus.DENIED;
    mNavigationManager.setUserLocationTrackingConsentStatus(userLocationTrackingConsentStatus);

    return mNavigationManager;
}

Finally, in the NavigationActivity, once the NavigationManager instance has been initialized by the (connected) Service, we can simply invoke the actual startNavigation method -- with the current route to be navigated:

@Override
public void onServiceConnected(ComponentName name, IBinder binder) {

    mNotificationService = NavigationNotificationService.fromBinder(binder);
    mNotificationService.setRoute(mRoute);

    mNavigationManager = mNotificationService.getNavigationManager();
    addNavigationListeners(mNavigationManager);

    mNavigationManager.startNavigation(mRoute);

    // ...
}

Refer to the complete implementation in the sample app for further details on all of the above.

Supporting Documentation