diff --git a/platform/android/MapboxGLDownstreamTestApp/.gitignore b/platform/android/MapboxGLDownstreamTestApp/.gitignore
new file mode 100644
index 00000000000..796b96d1c40
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/platform/android/MapboxGLDownstreamTestApp/build.gradle b/platform/android/MapboxGLDownstreamTestApp/build.gradle
new file mode 100644
index 00000000000..9046bb27880
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/build.gradle
@@ -0,0 +1,60 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 28
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ def getGitHash = { ->
+ try {
+ def stdout = new ByteArrayOutputStream()
+ exec {
+ commandLine 'git', 'rev-parse', '--short', 'HEAD'
+ standardOutput = stdout
+ }
+ return stdout.toString().trim()
+ } catch (Exception exception) {
+ return ""
+ }
+ }
+
+ defaultConfig {
+ applicationId "com.mapbox.mapboxsdk.downstream.testapp"
+ minSdkVersion 14
+ targetSdkVersion 28
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
+
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+}
+
+dependencies {
+ api(project(':MapboxGLAndroidSDK'))
+ implementation("com.mapbox.mapboxsdk:mapbox-android-navigation-ui:0.34.0") {
+ exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-sdk'
+ }
+
+ // Butter Knife
+ implementation "com.jakewharton:butterknife:8.8.1"
+ annotationProcessor "com.jakewharton:butterknife-compiler:8.8.1"
+
+ implementation 'com.android.support:appcompat-v7:28.0.0'
+ implementation 'com.android.support.constraint:constraint-layout:1.1.3'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'com.android.support.test:runner:1.0.2'
+ androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+}
diff --git a/platform/android/MapboxGLDownstreamTestApp/proguard-rules.pro b/platform/android/MapboxGLDownstreamTestApp/proguard-rules.pro
new file mode 100644
index 00000000000..f1b424510da
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/androidTest/java/com/mapbox/mapboxsdk/downstream/testapp/ExampleInstrumentedTest.java b/platform/android/MapboxGLDownstreamTestApp/src/androidTest/java/com/mapbox/mapboxsdk/downstream/testapp/ExampleInstrumentedTest.java
new file mode 100644
index 00000000000..8fcdee1a988
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/androidTest/java/com/mapbox/mapboxsdk/downstream/testapp/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.mapbox.mapboxsdk.downstream.testapp;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("com.mapbox.mapboxsdk.downstream.testapp", appContext.getPackageName());
+ }
+}
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/AndroidManifest.xml b/platform/android/MapboxGLDownstreamTestApp/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..f62d77f83d8
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/AndroidManifest.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/java/com/mapbox/mapboxsdk/downstream/testapp/NavigationLauncherActivity.java b/platform/android/MapboxGLDownstreamTestApp/src/main/java/com/mapbox/mapboxsdk/downstream/testapp/NavigationLauncherActivity.java
new file mode 100644
index 00000000000..0c04625696f
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/java/com/mapbox/mapboxsdk/downstream/testapp/NavigationLauncherActivity.java
@@ -0,0 +1,426 @@
+package com.mapbox.mapboxsdk.downstream.testapp;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.location.Location;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.design.widget.Snackbar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ProgressBar;
+import android.widget.Toast;
+
+import com.mapbox.android.core.location.LocationEngine;
+import com.mapbox.android.core.location.LocationEngineCallback;
+import com.mapbox.android.core.location.LocationEngineProvider;
+import com.mapbox.android.core.location.LocationEngineRequest;
+import com.mapbox.android.core.location.LocationEngineResult;
+import com.mapbox.api.directions.v5.DirectionsCriteria;
+import com.mapbox.api.directions.v5.models.DirectionsResponse;
+import com.mapbox.api.directions.v5.models.DirectionsRoute;
+import com.mapbox.core.constants.Constants;
+import com.mapbox.geojson.LineString;
+import com.mapbox.geojson.Point;
+import com.mapbox.mapboxsdk.Mapbox;
+import com.mapbox.mapboxsdk.annotations.Marker;
+import com.mapbox.mapboxsdk.annotations.MarkerOptions;
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.exceptions.InvalidLatLngBoundsException;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngBounds;
+import com.mapbox.mapboxsdk.location.LocationComponent;
+import com.mapbox.mapboxsdk.location.modes.RenderMode;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.maps.Style;
+import com.mapbox.services.android.navigation.ui.v5.NavigationLauncher;
+import com.mapbox.services.android.navigation.ui.v5.NavigationLauncherOptions;
+import com.mapbox.services.android.navigation.ui.v5.route.NavigationMapRoute;
+import com.mapbox.services.android.navigation.ui.v5.route.OnRouteSelectionChangeListener;
+import com.mapbox.services.android.navigation.v5.navigation.NavigationRoute;
+import com.mapbox.services.android.navigation.v5.utils.LocaleUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import butterknife.OnClick;
+import retrofit2.Call;
+import retrofit2.Response;
+import timber.log.Timber;
+
+public class NavigationLauncherActivity extends AppCompatActivity implements OnMapReadyCallback,
+ MapboxMap.OnMapLongClickListener, OnRouteSelectionChangeListener {
+
+ private static final int CAMERA_ANIMATION_DURATION = 1000;
+ private static final int DEFAULT_CAMERA_ZOOM = 16;
+ private static final int CHANGE_SETTING_REQUEST_CODE = 1;
+ private static final int INITIAL_ZOOM = 16;
+ private static final long UPDATE_INTERVAL_IN_MILLISECONDS = 1000;
+ private static final long FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS = 500;
+
+ private final NavigationLauncherLocationCallback callback = new NavigationLauncherLocationCallback(this);
+ private LocationEngine locationEngine;
+ private NavigationMapRoute mapRoute;
+ private MapboxMap mapboxMap;
+ private Marker currentMarker;
+ private Point currentLocation;
+ private Point destination;
+ private DirectionsRoute route;
+ private LocaleUtils localeUtils;
+ private boolean locationFound;
+
+ @BindView(R.id.mapView)
+ MapView mapView;
+ @BindView(R.id.launch_route_btn)
+ Button launchRouteBtn;
+ @BindView(R.id.loading)
+ ProgressBar loading;
+ @BindView(R.id.launch_btn_frame)
+ FrameLayout launchBtnFrame;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Mapbox.getInstance(getApplicationContext(), getString(R.string.mapbox_access_token));
+ setContentView(R.layout.activity_navigation_launcher);
+ ButterKnife.bind(this);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ localeUtils = new LocaleUtils();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.navigation_view_activity_menu, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.settings:
+ showSettings();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == CHANGE_SETTING_REQUEST_CODE && resultCode == RESULT_OK) {
+ boolean shouldRefetch = data.getBooleanExtra(NavigationSettingsActivity.UNIT_TYPE_CHANGED, false)
+ || data.getBooleanExtra(NavigationSettingsActivity.LANGUAGE_CHANGED, false);
+ if (destination != null && shouldRefetch) {
+ fetchRoute();
+ }
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @SuppressWarnings( {"MissingPermission"})
+ @Override
+ public void onResume() {
+ super.onResume();
+ mapView.onResume();
+ if (locationEngine != null) {
+ locationEngine.requestLocationUpdates(buildEngineRequest(), callback, null);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mapView.onPause();
+ if (locationEngine != null) {
+ locationEngine.removeLocationUpdates(callback);
+ }
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @OnClick(R.id.launch_route_btn)
+ public void onRouteLaunchClick() {
+ launchNavigationWithRoute();
+ }
+
+ @Override
+ public void onMapReady(MapboxMap mapboxMap) {
+ this.mapboxMap = mapboxMap;
+ mapboxMap.setStyle(Style.MAPBOX_STREETS, style -> {
+ mapboxMap.addOnMapLongClickListener(this);
+ initializeLocationEngine();
+ initializeLocationComponent(style);
+ initializeMapRoute();
+ });
+ }
+
+ @Override
+ public boolean onMapLongClick(@NonNull LatLng point) {
+ destination = Point.fromLngLat(point.getLongitude(), point.getLatitude());
+ launchRouteBtn.setEnabled(false);
+ loading.setVisibility(View.VISIBLE);
+ setCurrentMarkerPosition(point);
+ if (currentLocation != null) {
+ fetchRoute();
+ }
+ return false;
+ }
+
+ @Override
+ public void onNewPrimaryRouteSelected(DirectionsRoute directionsRoute) {
+ route = directionsRoute;
+ }
+
+ void updateCurrentLocation(Point currentLocation) {
+ this.currentLocation = currentLocation;
+ }
+
+ void onLocationFound(Location location) {
+ if (!locationFound) {
+ animateCamera(new LatLng(location.getLatitude(), location.getLongitude()));
+ Snackbar.make(mapView, R.string.explanation_long_press_waypoint, Snackbar.LENGTH_LONG).show();
+ locationFound = true;
+ hideLoading();
+ }
+ }
+
+ private void showSettings() {
+ startActivityForResult(new Intent(this, NavigationSettingsActivity.class), CHANGE_SETTING_REQUEST_CODE);
+ }
+
+ @SuppressWarnings( {"MissingPermission"})
+ private void initializeLocationEngine() {
+ locationEngine = LocationEngineProvider.getBestLocationEngine(getApplicationContext());
+ LocationEngineRequest request = buildEngineRequest();
+ locationEngine.requestLocationUpdates(request, callback, null);
+ locationEngine.getLastLocation(callback);
+ }
+
+ @SuppressWarnings( {"MissingPermission"})
+ private void initializeLocationComponent(Style style) {
+ LocationComponent locationComponent = mapboxMap.getLocationComponent();
+ locationComponent.activateLocationComponent(this, style, locationEngine);
+ locationComponent.setLocationComponentEnabled(true);
+ locationComponent.setRenderMode(RenderMode.COMPASS);
+ }
+
+ private void initializeMapRoute() {
+ mapRoute = new NavigationMapRoute(mapView, mapboxMap);
+ mapRoute.setOnRouteSelectionChangeListener(this);
+ }
+
+ private void fetchRoute() {
+ NavigationRoute.Builder builder = NavigationRoute.builder(this)
+ .accessToken(Mapbox.getAccessToken())
+ .origin(currentLocation)
+ .destination(destination)
+ .profile(getRouteProfileFromSharedPreferences())
+ .alternatives(true);
+ setFieldsFromSharedPreferences(builder);
+ builder.build()
+ .getRoute(new SimplifiedCallback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (validRouteResponse(response)) {
+ hideLoading();
+ route = response.body().routes().get(0);
+ if (route.distance() > 25d) {
+ launchRouteBtn.setEnabled(true);
+ mapRoute.addRoutes(response.body().routes());
+ boundCameraToRoute();
+ } else {
+ Snackbar.make(mapView, R.string.error_select_longer_route, Snackbar.LENGTH_SHORT).show();
+ }
+ }
+ }
+ });
+ loading.setVisibility(View.VISIBLE);
+ }
+
+ private void setFieldsFromSharedPreferences(NavigationRoute.Builder builder) {
+ builder
+ .language(getLanguageFromSharedPreferences())
+ .voiceUnits(getUnitTypeFromSharedPreferences());
+ }
+
+ private String getUnitTypeFromSharedPreferences() {
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ String defaultUnitType = getString(R.string.default_unit_type);
+ String unitType = sharedPreferences.getString(getString(R.string.unit_type_key), defaultUnitType);
+ if (unitType.equals(defaultUnitType)) {
+ unitType = localeUtils.getUnitTypeForDeviceLocale(this);
+ }
+
+ return unitType;
+ }
+
+ private Locale getLanguageFromSharedPreferences() {
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ String defaultLanguage = getString(R.string.default_locale);
+ String language = sharedPreferences.getString(getString(R.string.language_key), defaultLanguage);
+ if (language.equals(defaultLanguage)) {
+ return localeUtils.inferDeviceLocale(this);
+ } else {
+ return new Locale(language);
+ }
+ }
+
+ private boolean getShouldSimulateRouteFromSharedPreferences() {
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ return sharedPreferences.getBoolean(getString(R.string.simulate_route_key), false);
+ }
+
+ private String getRouteProfileFromSharedPreferences() {
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ return sharedPreferences.getString(
+ getString(R.string.route_profile_key), DirectionsCriteria.PROFILE_DRIVING_TRAFFIC
+ );
+ }
+
+ private void launchNavigationWithRoute() {
+ if (route == null) {
+ Snackbar.make(mapView, R.string.error_route_not_available, Snackbar.LENGTH_SHORT).show();
+ return;
+ }
+
+ NavigationLauncherOptions.Builder optionsBuilder = NavigationLauncherOptions.builder()
+ .shouldSimulateRoute(getShouldSimulateRouteFromSharedPreferences());
+ CameraPosition initialPosition = new CameraPosition.Builder()
+ .target(new LatLng(currentLocation.latitude(), currentLocation.longitude()))
+ .zoom(INITIAL_ZOOM)
+ .build();
+ optionsBuilder.initialMapCameraPosition(initialPosition);
+ optionsBuilder.directionsRoute(route);
+ NavigationLauncher.startNavigation(this, optionsBuilder.build());
+ }
+
+ private boolean validRouteResponse(Response response) {
+ return response.body() != null && !response.body().routes().isEmpty();
+ }
+
+ private void hideLoading() {
+ if (loading.getVisibility() == View.VISIBLE) {
+ loading.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ public void boundCameraToRoute() {
+ if (route != null) {
+ List routeCoords = LineString.fromPolyline(route.geometry(),
+ Constants.PRECISION_6).coordinates();
+ List bboxPoints = new ArrayList<>();
+ for (Point point : routeCoords) {
+ bboxPoints.add(new LatLng(point.latitude(), point.longitude()));
+ }
+ if (bboxPoints.size() > 1) {
+ try {
+ LatLngBounds bounds = new LatLngBounds.Builder().includes(bboxPoints).build();
+ // left, top, right, bottom
+ int topPadding = launchBtnFrame.getHeight() * 2;
+ animateCameraBbox(bounds, CAMERA_ANIMATION_DURATION, new int[] {50, topPadding, 50, 100});
+ } catch (InvalidLatLngBoundsException exception) {
+ Toast.makeText(this, R.string.error_valid_route_not_found, Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+ }
+
+ private void animateCameraBbox(LatLngBounds bounds, int animationTime, int[] padding) {
+ CameraPosition position = mapboxMap.getCameraForLatLngBounds(bounds, padding);
+ mapboxMap.animateCamera(CameraUpdateFactory.newCameraPosition(position), animationTime);
+ }
+
+ private void animateCamera(LatLng point) {
+ mapboxMap.animateCamera(CameraUpdateFactory.newLatLngZoom(point, DEFAULT_CAMERA_ZOOM), CAMERA_ANIMATION_DURATION);
+ }
+
+ private void setCurrentMarkerPosition(LatLng position) {
+ if (position != null) {
+ if (currentMarker == null) {
+ MarkerOptions markerViewOptions = new MarkerOptions()
+ .position(position);
+ currentMarker = mapboxMap.addMarker(markerViewOptions);
+ } else {
+ currentMarker.setPosition(position);
+ }
+ }
+ }
+
+ @NonNull
+ private LocationEngineRequest buildEngineRequest() {
+ return new LocationEngineRequest.Builder(UPDATE_INTERVAL_IN_MILLISECONDS)
+ .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY)
+ .setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS)
+ .build();
+ }
+
+ private static class NavigationLauncherLocationCallback implements LocationEngineCallback {
+
+ private final WeakReference activityWeakReference;
+
+ NavigationLauncherLocationCallback(NavigationLauncherActivity activity) {
+ this.activityWeakReference = new WeakReference<>(activity);
+ }
+
+ @Override
+ public void onSuccess(LocationEngineResult result) {
+ NavigationLauncherActivity activity = activityWeakReference.get();
+ if (activity != null) {
+ Location location = result.getLastLocation();
+ if (location == null) {
+ return;
+ }
+ activity.updateCurrentLocation(Point.fromLngLat(location.getLongitude(), location.getLatitude()));
+ activity.onLocationFound(location);
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Exception exception) {
+ Timber.e(exception);
+ }
+ }
+}
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/java/com/mapbox/mapboxsdk/downstream/testapp/NavigationSettingsActivity.java b/platform/android/MapboxGLDownstreamTestApp/src/main/java/com/mapbox/mapboxsdk/downstream/testapp/NavigationSettingsActivity.java
new file mode 100644
index 00000000000..1441f367f52
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/java/com/mapbox/mapboxsdk/downstream/testapp/NavigationSettingsActivity.java
@@ -0,0 +1,94 @@
+package com.mapbox.mapboxsdk.downstream.testapp;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.Environment;
+import android.preference.ListPreference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class NavigationSettingsActivity extends PreferenceActivity {
+
+ public static final String UNIT_TYPE_CHANGED = "unit_type_changed";
+ public static final String LANGUAGE_CHANGED = "language_changed";
+ public static final String OFFLINE_CHANGED = "offline_changed";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, key) -> {
+ Intent resultIntent = new Intent();
+ resultIntent.putExtra(UNIT_TYPE_CHANGED, key.equals(getString(R.string.unit_type_key)));
+ resultIntent.putExtra(LANGUAGE_CHANGED, key.equals(getString(R.string.language_key)));
+ resultIntent.putExtra(OFFLINE_CHANGED, key.equals(getString(R.string.offline_version_key)));
+ setResult(RESULT_OK, resultIntent);
+ };
+
+ PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(listener);
+ getFragmentManager().beginTransaction().replace(
+ android.R.id.content, new NavigationViewPreferenceFragment()
+ ).commit();
+ }
+
+ @Override
+ protected boolean isValidFragment(String fragmentName) {
+ return super.isValidFragment(fragmentName);
+ }
+
+ public static class NavigationViewPreferenceFragment extends PreferenceFragment {
+
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.fragment_navigation_preferences);
+ String gitHashTitle = String.format("Last Commit Hash: %s", BuildConfig.GIT_HASH);
+ findPreference(getString(R.string.git_hash_key)).setTitle(gitHashTitle);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ getOfflineVersions();
+ PreferenceManager.setDefaultValues(getActivity(), R.xml.fragment_navigation_preferences, false);
+ }
+
+ private void getOfflineVersions() {
+ File file = new File(Environment.getExternalStoragePublicDirectory("Offline"), "tiles");
+ if (!file.exists()) {
+ file.mkdirs();
+ }
+
+ ListPreference offlineVersions = (ListPreference) findPreference(getString(R.string.offline_version_key));
+ List list = buildFileList(file);
+ if (!list.isEmpty()) {
+ String[] entries = list.toArray(new String[list.size() - 1]);
+ offlineVersions.setEntries(entries);
+ offlineVersions.setEntryValues(entries);
+ offlineVersions.setEnabled(true);
+ } else {
+ offlineVersions.setEnabled(false);
+ }
+ }
+
+ @NonNull
+ private List buildFileList(File file) {
+ List list;
+ if (file.list() != null && file.list().length != 0) {
+ list = new ArrayList<>(Arrays.asList(file.list()));
+ } else {
+ list = new ArrayList<>();
+ }
+ return list;
+ }
+ }
+}
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/java/com/mapbox/mapboxsdk/downstream/testapp/SimplifiedCallback.java b/platform/android/MapboxGLDownstreamTestApp/src/main/java/com/mapbox/mapboxsdk/downstream/testapp/SimplifiedCallback.java
new file mode 100644
index 00000000000..95019b45699
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/java/com/mapbox/mapboxsdk/downstream/testapp/SimplifiedCallback.java
@@ -0,0 +1,18 @@
+package com.mapbox.mapboxsdk.downstream.testapp;
+
+import com.mapbox.api.directions.v5.models.DirectionsResponse;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import timber.log.Timber;
+
+/**
+ * Helper class to reduce redundant logging code when no other action is taken in onFailure
+ */
+public abstract class SimplifiedCallback implements Callback {
+ @Override
+ public void onFailure(Call call, Throwable throwable) {
+ Timber.e(throwable, throwable.getMessage());
+ }
+}
+
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/drawable-v24/ic_launcher_foreground.xml b/platform/android/MapboxGLDownstreamTestApp/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000000..485bbd377e2
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/drawable/demo_switch_background.xml b/platform/android/MapboxGLDownstreamTestApp/src/main/res/drawable/demo_switch_background.xml
new file mode 100644
index 00000000000..a5ceeec08b6
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/res/drawable/demo_switch_background.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/drawable/ic_launcher_background.xml b/platform/android/MapboxGLDownstreamTestApp/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000000..7ca09099aa5
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/drawable/ic_settings.xml b/platform/android/MapboxGLDownstreamTestApp/src/main/res/drawable/ic_settings.xml
new file mode 100644
index 00000000000..f98df97aec4
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/res/drawable/ic_settings.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/layout/activity_navigation_launcher.xml b/platform/android/MapboxGLDownstreamTestApp/src/main/res/layout/activity_navigation_launcher.xml
new file mode 100644
index 00000000000..94d2f1bec7b
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/res/layout/activity_navigation_launcher.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/menu/navigation_view_activity_menu.xml b/platform/android/MapboxGLDownstreamTestApp/src/main/res/menu/navigation_view_activity_menu.xml
new file mode 100644
index 00000000000..6bad267647d
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/res/menu/navigation_view_activity_menu.xml
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000000..bbd3e021239
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000000..bbd3e021239
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-hdpi/ic_launcher.png b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000000..898f3ed59ac
Binary files /dev/null and b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-hdpi/ic_launcher_round.png b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000000..dffca3601eb
Binary files /dev/null and b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-mdpi/ic_launcher.png b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000000..64ba76f75e9
Binary files /dev/null and b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-mdpi/ic_launcher_round.png b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000000..dae5e082342
Binary files /dev/null and b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xhdpi/ic_launcher.png b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000000..e5ed46597ea
Binary files /dev/null and b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000000..14ed0af3502
Binary files /dev/null and b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xxhdpi/ic_launcher.png b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000000..b0907cac3bf
Binary files /dev/null and b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000000..d8ae0315497
Binary files /dev/null and b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000000..2c18de9e661
Binary files /dev/null and b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000000..beed3cdd2c3
Binary files /dev/null and b/platform/android/MapboxGLDownstreamTestApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/values/arrays.xml b/platform/android/MapboxGLDownstreamTestApp/src/main/res/values/arrays.xml
new file mode 100644
index 00000000000..b1d336bdce7
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/res/values/arrays.xml
@@ -0,0 +1,43 @@
+
+
+
+
+ - Default for Device
+ - English
+ - French
+ - German
+ - Indonesian
+
+
+
+ - @string/default_locale
+ - en
+ - fr
+ - de
+ - in
+
+
+
+ - Default for Device
+ - Metric
+ - Imperial
+
+
+
+ - @string/default_unit_type
+ - metric
+ - imperial
+
+
+
+ - Driving
+ - Cycling
+ - Walking
+
+
+
+ - driving-traffic
+ - cycling
+ - walking
+
+
\ No newline at end of file
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/values/colors.xml b/platform/android/MapboxGLDownstreamTestApp/src/main/res/values/colors.xml
new file mode 100644
index 00000000000..69b22338c65
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #008577
+ #00574B
+ #D81B60
+
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/values/strings.xml b/platform/android/MapboxGLDownstreamTestApp/src/main/res/values/strings.xml
new file mode 100644
index 00000000000..253633d6839
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/res/values/strings.xml
@@ -0,0 +1,28 @@
+
+ MapboxGLDownstreamTestApp
+ unit_type
+ language
+ offline_preference_key
+ YOUR_ACCESS_TOKEN_GOES_HERE
+ view_examples
+ git_hash
+ General
+ simulate_route
+ Simulate Route
+ default_for_device
+ default_for_device
+ Language
+ Unit Type
+ route_profile
+ Route Profile
+ Offline
+ offline_region_download
+ Offline routing enabled
+ offline_region_download
+ Download offline region
+ Choose offline version
+ Long press map to place waypoint
+ Current route is not available
+ Please select a longer route
+ Valid route not found.
+
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/values/styles.xml b/platform/android/MapboxGLDownstreamTestApp/src/main/res/values/styles.xml
new file mode 100644
index 00000000000..5885930df6d
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/main/res/xml/fragment_navigation_preferences.xml b/platform/android/MapboxGLDownstreamTestApp/src/main/res/xml/fragment_navigation_preferences.xml
new file mode 100644
index 00000000000..bf13722a6a6
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/main/res/xml/fragment_navigation_preferences.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/android/MapboxGLDownstreamTestApp/src/test/java/com/mapbox/mapboxsdk/downstream/testapp/ExampleUnitTest.java b/platform/android/MapboxGLDownstreamTestApp/src/test/java/com/mapbox/mapboxsdk/downstream/testapp/ExampleUnitTest.java
new file mode 100644
index 00000000000..9c1f07e3007
--- /dev/null
+++ b/platform/android/MapboxGLDownstreamTestApp/src/test/java/com/mapbox/mapboxsdk/downstream/testapp/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.mapbox.mapboxsdk.downstream.testapp;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/platform/android/settings.gradle b/platform/android/settings.gradle
index c0315fed04f..0444718a88d 100644
--- a/platform/android/settings.gradle
+++ b/platform/android/settings.gradle
@@ -1 +1 @@
-include ':MapboxGLAndroidSDK', ':MapboxGLAndroidSDKTestApp', ':MapboxGLAndroidSDKLint'
\ No newline at end of file
+include ':MapboxGLAndroidSDK', ':MapboxGLAndroidSDKTestApp', ':MapboxGLAndroidSDKLint', ':MapboxGLDownstreamTestApp'
\ No newline at end of file