diff --git a/CHANGELOG.md b/CHANGELOG.md index a3863bbf56a..ed3df274bea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ Mapbox welcomes participation and contributions from everyone. -### v0.38.0 - May 15, 2019 +### v0.38.0 - May 16, 2019 +* Add option to load offline maps database for NavigationView [#1895](https://github.com/mapbox/mapbox-navigation-android/pull/1895) * Update Maps SDK to 7.4.0 [#1907](https://github.com/mapbox/mapbox-navigation-android/pull/1907) * Added walking options [#1934](https://github.com/mapbox/mapbox-navigation-android/pull/1934) * SoundButton clicklistener wasn't set properly [#1937](https://github.com/mapbox/mapbox-navigation-android/pull/1937) diff --git a/app/src/main/java/com/mapbox/services/android/navigation/testapp/activity/OfflineRegionDownloadActivity.kt b/app/src/main/java/com/mapbox/services/android/navigation/testapp/activity/OfflineRegionDownloadActivity.kt index bdd7cbb2fa0..7384908bfff 100644 --- a/app/src/main/java/com/mapbox/services/android/navigation/testapp/activity/OfflineRegionDownloadActivity.kt +++ b/app/src/main/java/com/mapbox/services/android/navigation/testapp/activity/OfflineRegionDownloadActivity.kt @@ -18,15 +18,20 @@ import android.widget.AdapterView.OnItemSelectedListener import android.widget.ArrayAdapter import android.widget.Toast import com.mapbox.geojson.BoundingBox +import com.mapbox.geojson.Geometry +import com.mapbox.geojson.gson.GeometryGeoJson import com.mapbox.mapboxsdk.Mapbox +import com.mapbox.mapboxsdk.geometry.LatLngBounds import com.mapbox.mapboxsdk.maps.MapboxMap import com.mapbox.mapboxsdk.maps.Style +import com.mapbox.mapboxsdk.offline.* import com.mapbox.mapboxsdk.style.layers.FillLayer import com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillColor import com.mapbox.mapboxsdk.style.sources.GeoJsonSource import com.mapbox.services.android.navigation.testapp.R import com.mapbox.services.android.navigation.v5.navigation.* import kotlinx.android.synthetic.main.activity_offline_region_download.* +import org.json.JSONObject import timber.log.Timber class OfflineRegionDownloadActivity : AppCompatActivity(), RouteTileDownloadListener, OnOfflineTilesRemovedCallback { @@ -55,6 +60,40 @@ class OfflineRegionDownloadActivity : AppCompatActivity(), RouteTileDownloadList get() { return MapboxOfflineRouter(obtainOfflineDirectory()) } + private lateinit var offlineManager: OfflineManager + private var offlineRegion: OfflineRegion? = null + private val offlineRegionCallback = object : OfflineManager.CreateOfflineRegionCallback { + override fun onCreate(offlineRegion: OfflineRegion?) { + Timber.d("Offline region created: %s", "NavigationOfflineMapsRegion") + this@OfflineRegionDownloadActivity.offlineRegion = offlineRegion + launchMapsDownload() + } + + override fun onError(error: String?) { + Timber.e("Error: %s", error) + } + } + + private val offlineRegionObserver = object : OfflineRegion.OfflineRegionObserver { + override fun mapboxTileCountLimitExceeded(limit: Long) { + Timber.e("Mapbox tile count limit exceeded: %s", limit) + } + + override fun onStatusChanged(status: OfflineRegionStatus?) { + Timber.d("%s/%s resources; %s bytes downloaded.", + status?.completedResourceCount, + status?.requiredResourceCount, + status?.completedResourceSize) + if (status?.isComplete!!) { + downloadSelectedRegion() + } + } + + override fun onError(error: OfflineRegionError?) { + Timber.e("onError reason: %s", error?.reason) + Timber.e("onError message: %s", error?.message) + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -124,6 +163,7 @@ class OfflineRegionDownloadActivity : AppCompatActivity(), RouteTileDownloadList it.addSource(GeoJsonSource("bounding-box-source")) it.addLayer(FillLayer("bounding-box-layer", "bounding-box-source") .withProperties(fillColor(Color.parseColor("#50667F")))) + offlineManager = OfflineManager.getInstance(this) } this.mapboxMap = mapboxMap mapboxMap.uiSettings.isRotateGesturesEnabled = false @@ -140,7 +180,7 @@ class OfflineRegionDownloadActivity : AppCompatActivity(), RouteTileDownloadList this, WRITE_EXTERNAL_STORAGE) != PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, arrayOf(WRITE_EXTERNAL_STORAGE), 1) } else { - downloadSelectedRegion() + downloadMapsRegion() } } @@ -157,7 +197,7 @@ class OfflineRegionDownloadActivity : AppCompatActivity(), RouteTileDownloadList when (requestCode) { EXTERNAL_STORAGE_PERMISSION -> { if ((grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED)) { - downloadSelectedRegion() + downloadMapsRegion() } else { setDownloadButtonEnabled(false) } @@ -165,8 +205,40 @@ class OfflineRegionDownloadActivity : AppCompatActivity(), RouteTileDownloadList } } - private fun downloadSelectedRegion() { + private fun downloadMapsRegion() { showDownloading(false, "Requesting tiles....") + // TODO Hardcoding OfflineRegionDefinitionProvider values for testing / debugging purposes + val styleUrl: String? = "mapbox://styles/mapbox/navigation-guidance-day-v4" + //val styleUrl: String? = mapboxMap.style?.url + val bounds: LatLngBounds = LatLngBounds.from(boundingBox.north(), boundingBox.east(), boundingBox.south(), boundingBox.west()) + // TODO Testing downloading a Geometry + val geometry: Geometry = GeometryGeoJson.fromJson("{\"type\":\"Polygon\",\"coordinates\":[[[-77.152533,39.085537],[-77.152533,39.083038],[-77.150031,39.083038],[-77.150031,39.085537],[-77.147529,39.085537],[-77.147529,39.088039],[-77.147529,39.090538],[-77.150031,39.090538],[-77.150031,39.093037],[-77.150031,39.095539],[-77.150031,39.098038],[-77.150031,39.100540],[-77.150031,39.103039],[-77.152533,39.103039],[-77.152533,39.105537],[-77.155028,39.105537],[-77.155028,39.108040],[-77.155028,39.110538],[-77.157531,39.110538],[-77.157531,39.113037],[-77.160033,39.113037],[-77.160033,39.115536],[-77.162528,39.115540],[-77.162528,39.118038],[-77.165030,39.118038],[-77.165030,39.115536],[-77.167533,39.115536],[-77.167533,39.113037],[-77.167533,39.110538],[-77.165030,39.110538],[-77.165030,39.108040],[-77.162536,39.108036],[-77.162536,39.105537],[-77.162536,39.103039],[-77.160033,39.103039],[-77.160033,39.100540],[-77.157531,39.100536],[-77.157531,39.098038],[-77.157531,39.095535],[-77.157531,39.093037],[-77.157531,39.090538],[-77.157531,39.088039],[-77.155036,39.088036],[-77.155036,39.085537],[-77.152533,39.085537]]]}") + // TODO Hardcoding OfflineRegionDefinitionProvider values for testing / debugging purposes + val minZoom: Double = 11.0 + val maxZoom: Double = 17.0 + //val minZoom: Double = mapboxMap.cameraPosition.zoom + //val maxZoom: Double = mapboxMap.maxZoomLevel + val pixelRatio: Float = this.resources.displayMetrics.density + val definition: OfflineTilePyramidRegionDefinition = OfflineTilePyramidRegionDefinition( + styleUrl, bounds, minZoom, maxZoom, pixelRatio) + // TODO Testing downloading a Geometry using OfflineGeometryRegionDefinition as definition + //val definition: OfflineGeometryRegionDefinition = OfflineGeometryRegionDefinition( + // styleUrl, geometry, minZoom, maxZoom, pixelRatio) + + val metadata: ByteArray + val jsonObject: JSONObject = JSONObject() + jsonObject.put("FIELD_REGION_NAME", "NavigationOfflineMapsRegion") + val json: String = jsonObject.toString() + metadata = json.toByteArray() + offlineManager.createOfflineRegion(definition, metadata, offlineRegionCallback) + } + + private fun launchMapsDownload() { + offlineRegion?.setObserver(offlineRegionObserver) + offlineRegion?.setDownloadState(OfflineRegion.STATE_ACTIVE) + } + + private fun downloadSelectedRegion() { val builder = OfflineTiles.builder() .accessToken(Mapbox.getAccessToken()) .version(versionSpinner.selectedItem as String) @@ -278,6 +350,7 @@ class OfflineRegionDownloadActivity : AppCompatActivity(), RouteTileDownloadList override fun onDestroy() { super.onDestroy() + offlineRegion?.setObserver(null) mapView.onDestroy() } diff --git a/app/src/main/java/com/mapbox/services/android/navigation/testapp/activity/navigationui/NavigationLauncherActivity.java b/app/src/main/java/com/mapbox/services/android/navigation/testapp/activity/navigationui/NavigationLauncherActivity.java index b26f521b9a1..9abfe9beed9 100644 --- a/app/src/main/java/com/mapbox/services/android/navigation/testapp/activity/navigationui/NavigationLauncherActivity.java +++ b/app/src/main/java/com/mapbox/services/android/navigation/testapp/activity/navigationui/NavigationLauncherActivity.java @@ -354,6 +354,13 @@ private void launchNavigationWithRoute() { if (!offlineVersion.isEmpty()) { optionsBuilder.offlineRoutingTilesVersion(offlineVersion); } + // TODO Testing dynamic offline + /** + * File downloadDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + * String databaseFilePath = downloadDirectory + "/" + "kingfarm.db"; + * String offlineStyleUrl = "mapbox://styles/mapbox/navigation-guidance-day-v4"; + * optionsBuilder.offlineMapOptions(new MapOfflineOptions(databaseFilePath, offlineStyleUrl)); + */ NavigationLauncher.startNavigation(this, optionsBuilder.build()); } diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 6aa5325149a..9e035458a28 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -10,9 +10,9 @@ ext { version = [ mapboxMapSdk : '7.4.0', mapboxSdkServices : '4.8.0', - mapboxEvents : '4.3.0', + mapboxEvents : '4.4.1', mapboxCore : '1.2.0', - mapboxNavigator : '6.1.3', + mapboxNavigator : '6.2.0', mapboxCrashMonitor : '2.0.0', mapboxAnnotationPlugin : '0.6.0', mapboxSearchSdk : '0.1.0-SNAPSHOT', @@ -39,7 +39,8 @@ ext { ankoCommon : '0.10.0', firebaseCore : '16.0.7', crashlytics : '2.9.9', - multidex : '1.0.3' + multidex : '1.0.3', + json : '20180813' ] dependenciesList = [ @@ -100,6 +101,7 @@ ext { hamcrest : "org.hamcrest:hamcrest-junit:${version.hamcrest}", commonsIO : "commons-io:commons-io:${version.commonsIO}", robolectric : "org.robolectric:robolectric:${version.robolectric}", + json : "org.json:json:${version.json}", // play services gmsLocation : "com.google.android.gms:play-services-location:${version.gmsLocation}", diff --git a/libandroid-navigation-ui/build.gradle b/libandroid-navigation-ui/build.gradle index 234efdae2c4..4dd00c5f06d 100644 --- a/libandroid-navigation-ui/build.gradle +++ b/libandroid-navigation-ui/build.gradle @@ -76,6 +76,7 @@ dependencies { testImplementation dependenciesList.junit testImplementation dependenciesList.mockito testImplementation dependenciesList.robolectric + testImplementation dependenciesList.json } apply from: 'javadoc.gradle' diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/CreateOfflineRegionCallback.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/CreateOfflineRegionCallback.java new file mode 100644 index 00000000000..54cfb8fa247 --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/CreateOfflineRegionCallback.java @@ -0,0 +1,24 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import com.mapbox.mapboxsdk.offline.OfflineManager; +import com.mapbox.mapboxsdk.offline.OfflineRegion; + +class CreateOfflineRegionCallback implements OfflineManager.CreateOfflineRegionCallback { + + private final OfflineRegionDownloadCallback callback; + + CreateOfflineRegionCallback(OfflineRegionDownloadCallback callback) { + this.callback = callback; + } + + @Override + public void onCreate(OfflineRegion offlineRegion) { + offlineRegion.setDownloadState(OfflineRegion.STATE_ACTIVE); + offlineRegion.setObserver(new OfflineRegionObserver(callback)); + } + + @Override + public void onError(String error) { + callback.onError(error); + } +} diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MapConnectivityController.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MapConnectivityController.java new file mode 100644 index 00000000000..04b8beb78db --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MapConnectivityController.java @@ -0,0 +1,10 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import com.mapbox.mapboxsdk.Mapbox; + +class MapConnectivityController { + + void assign(Boolean state) { + Mapbox.setConnected(state); + } +} diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MapOfflineManager.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MapOfflineManager.java new file mode 100644 index 00000000000..da313ee0941 --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MapOfflineManager.java @@ -0,0 +1,73 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import android.location.Location; +import android.support.annotation.NonNull; + +import com.mapbox.geojson.Geometry; +import com.mapbox.mapboxsdk.offline.OfflineGeometryRegionDefinition; +import com.mapbox.mapboxsdk.offline.OfflineManager; +import com.mapbox.services.android.navigation.v5.routeprogress.ProgressChangeListener; +import com.mapbox.services.android.navigation.v5.routeprogress.RouteProgress; + +class MapOfflineManager implements ProgressChangeListener { + + private final OfflineManager offlineManager; + private final OfflineRegionDefinitionProvider definitionProvider; + private final OfflineMetadataProvider metadataProvider; + private final MapConnectivityController connectivityController; + private final RegionDownloadCallback regionDownloadCallback; + private Geometry previousRouteGeometry; + private MergeOfflineRegionsCallback mergeOfflineRegionsCallback; + + MapOfflineManager(OfflineManager offlineManager, OfflineRegionDefinitionProvider definitionProvider, + OfflineMetadataProvider metadataProvider, MapConnectivityController connectivityController, + RegionDownloadCallback regionDownloadCallback) { + this.offlineManager = offlineManager; + this.definitionProvider = definitionProvider; + this.metadataProvider = metadataProvider; + this.connectivityController = connectivityController; + this.regionDownloadCallback = regionDownloadCallback; + } + + // Package private (no modifier) for testing purposes + MapOfflineManager(OfflineManager offlineManager, OfflineRegionDefinitionProvider definitionProvider, + OfflineMetadataProvider metadataProvider, MapConnectivityController connectivityController, + RegionDownloadCallback regionDownloadCallback, + MergeOfflineRegionsCallback mergeOfflineRegionsCallback) { + this.offlineManager = offlineManager; + this.definitionProvider = definitionProvider; + this.metadataProvider = metadataProvider; + this.connectivityController = connectivityController; + this.regionDownloadCallback = regionDownloadCallback; + this.mergeOfflineRegionsCallback = mergeOfflineRegionsCallback; + } + + @Override + public void onProgressChange(Location location, RouteProgress routeProgress) { + Geometry currentRouteGeometry = routeProgress.routeGeometryWithBuffer(); + if (previousRouteGeometry == null || !previousRouteGeometry.equals(currentRouteGeometry)) { + previousRouteGeometry = currentRouteGeometry; + String routeSummary = routeProgress.directionsRoute().routeOptions().requestUuid(); + download(routeSummary, previousRouteGeometry, regionDownloadCallback); + } + } + + void loadDatabase(@NonNull String offlineDatabasePath, OfflineDatabaseLoadedCallback callback) { + mergeOfflineRegionsCallback = new MergeOfflineRegionsCallback(callback); + offlineManager.mergeOfflineRegions(offlineDatabasePath, mergeOfflineRegionsCallback); + } + + void onDestroy() { + if (mergeOfflineRegionsCallback != null) { + mergeOfflineRegionsCallback.onDestroy(); + } + } + + private void download(@NonNull String routeSummary, @NonNull Geometry routeGeometry, + final OfflineRegionDownloadCallback callback) { + OfflineGeometryRegionDefinition definition = definitionProvider.buildRegionFor(routeGeometry); + byte[] metadata = metadataProvider.buildMetadataFor(routeSummary); + connectivityController.assign(null); + offlineManager.createOfflineRegion(definition, metadata, new CreateOfflineRegionCallback(callback)); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MapOfflineOptions.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MapOfflineOptions.java new file mode 100644 index 00000000000..2163cdc23dc --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MapOfflineOptions.java @@ -0,0 +1,40 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import android.support.annotation.NonNull; + +public class MapOfflineOptions { + + private final String databasePath; + private final String styleUrl; + + /** + * Add an offline path and style URL for loading an offline map database. + * + * @param databaseFilePath to the offline database on the device + * @param styleUrl for the offline database data + */ + public MapOfflineOptions(@NonNull String databaseFilePath, @NonNull String styleUrl) { + this.databasePath = databaseFilePath; + this.styleUrl = styleUrl; + } + + /** + * The offline path to the offline map database. + * + * @return the database path + */ + @NonNull + public String getDatabasePath() { + return databasePath; + } + + /** + * The map style URL for the offline map database. + * + * @return the style URL + */ + @NonNull + public String getStyleUrl() { + return styleUrl; + } +} diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MapboxNavigationActivity.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MapboxNavigationActivity.java index 8ca73513876..34f7dbfa851 100644 --- a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MapboxNavigationActivity.java +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MapboxNavigationActivity.java @@ -139,6 +139,12 @@ private void extractConfiguration(NavigationViewOptions.Builder options) { if (!offlineVersion.isEmpty()) { options.offlineRoutingTilesVersion(offlineVersion); } + String offlineMapDatabasePath = preferences.getString(NavigationConstants.MAP_DATABASE_PATH_KEY, ""); + String offlineMapStyleUrl = preferences.getString(NavigationConstants.MAP_STYLE_URL_KEY, ""); + if (!offlineMapDatabasePath.isEmpty() && !offlineMapStyleUrl.isEmpty()) { + MapOfflineOptions mapOfflineOptions = new MapOfflineOptions(offlineMapDatabasePath, offlineMapStyleUrl); + options.offlineMapOptions(mapOfflineOptions); + } } private void finishNavigation() { diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MergeOfflineRegionsCallback.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MergeOfflineRegionsCallback.java new file mode 100644 index 00000000000..4cbde297931 --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MergeOfflineRegionsCallback.java @@ -0,0 +1,32 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import com.mapbox.mapboxsdk.offline.OfflineManager; +import com.mapbox.mapboxsdk.offline.OfflineRegion; + +class MergeOfflineRegionsCallback implements OfflineManager.MergeOfflineRegionsCallback { + + private OfflineDatabaseLoadedCallback callback; + + MergeOfflineRegionsCallback(OfflineDatabaseLoadedCallback callback) { + this.callback = callback; + } + + @Override + public void onMerge(OfflineRegion[] offlineRegions) { + if (callback != null) { + callback.onComplete(); + } + } + + @Override + public void onError(String error) { + if (callback != null) { + callback.onError(error); + } + } + + OfflineDatabaseLoadedCallback onDestroy() { + callback = null; + return callback; + } +} diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationLauncher.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationLauncher.java index cb80e1637ea..ada5e739896 100644 --- a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationLauncher.java +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationLauncher.java @@ -40,6 +40,10 @@ public static void startNavigation(Activity activity, NavigationLauncherOptions storeThemePreferences(options, editor); storeOfflinePath(options, editor); storeOfflineVersion(options, editor); + if (options.offlineMapOptions() != null) { + storeOfflineMapDatabasePath(options, editor); + storeOfflineMapStyleUrl(options, editor); + } editor.apply(); @@ -75,6 +79,8 @@ static void cleanUpPreferences(Context context) { .remove(NavigationConstants.NAVIGATION_VIEW_DARK_THEME) .remove(NavigationConstants.OFFLINE_PATH_KEY) .remove(NavigationConstants.OFFLINE_VERSION_KEY) + .remove(NavigationConstants.MAP_DATABASE_PATH_KEY) + .remove(NavigationConstants.MAP_STYLE_URL_KEY) .apply(); } @@ -116,4 +122,11 @@ private static void storeOfflineVersion(NavigationLauncherOptions options, Share editor.putString(NavigationConstants.OFFLINE_VERSION_KEY, options.offlineRoutingTilesVersion()); } + private static void storeOfflineMapDatabasePath(NavigationLauncherOptions options, SharedPreferences.Editor editor) { + editor.putString(NavigationConstants.MAP_DATABASE_PATH_KEY, options.offlineMapOptions().getDatabasePath()); + } + + private static void storeOfflineMapStyleUrl(NavigationLauncherOptions options, SharedPreferences.Editor editor) { + editor.putString(NavigationConstants.MAP_STYLE_URL_KEY, options.offlineMapOptions().getStyleUrl()); + } } diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationLauncherOptions.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationLauncherOptions.java index b2efa24d252..8a4cd9a938b 100644 --- a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationLauncherOptions.java +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationLauncherOptions.java @@ -49,6 +49,14 @@ public abstract static class Builder { */ public abstract Builder offlineRoutingTilesVersion(String offlineVersion); + /** + * Add options to configure offline maps. + * + * @param mapOfflineOptions for offline configuration + * @return this builder + */ + public abstract Builder offlineMapOptions(MapOfflineOptions mapOfflineOptions); + public abstract NavigationLauncherOptions build(); } diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationOfflineDatabaseCallback.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationOfflineDatabaseCallback.java new file mode 100644 index 00000000000..97a8a0da93f --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationOfflineDatabaseCallback.java @@ -0,0 +1,30 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import com.mapbox.services.android.navigation.v5.navigation.MapboxNavigation; + +import timber.log.Timber; + +class NavigationOfflineDatabaseCallback implements OfflineDatabaseLoadedCallback { + + private final MapboxNavigation navigation; + private final MapOfflineManager mapOfflineManager; + + NavigationOfflineDatabaseCallback(MapboxNavigation navigation, MapOfflineManager mapOfflineManager) { + this.navigation = navigation; + this.mapOfflineManager = mapOfflineManager; + } + + @Override + public void onComplete() { + navigation.addProgressChangeListener(mapOfflineManager); + } + + @Override + public void onError(String error) { + Timber.e(error); + } + + void onDestroy() { + mapOfflineManager.onDestroy(); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationUiOptions.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationUiOptions.java index 2dd93fb0701..4a26127a1e5 100644 --- a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationUiOptions.java +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationUiOptions.java @@ -23,4 +23,7 @@ public abstract class NavigationUiOptions { @Nullable public abstract String offlineRoutingTilesVersion(); + + @Nullable + public abstract MapOfflineOptions offlineMapOptions(); } diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModel.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModel.java index 1518ac83366..999a4b88346 100644 --- a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModel.java +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModel.java @@ -15,6 +15,7 @@ import com.mapbox.api.directions.v5.models.RouteOptions; import com.mapbox.geojson.Point; import com.mapbox.mapboxsdk.Mapbox; +import com.mapbox.mapboxsdk.offline.OfflineManager; import com.mapbox.services.android.navigation.ui.v5.camera.DynamicCamera; import com.mapbox.services.android.navigation.ui.v5.feedback.FeedbackItem; import com.mapbox.services.android.navigation.ui.v5.instruction.BannerInstructionModel; @@ -38,7 +39,6 @@ import com.mapbox.services.android.navigation.v5.offroute.OffRouteListener; import com.mapbox.services.android.navigation.v5.route.FasterRouteListener; import com.mapbox.services.android.navigation.v5.route.RouteFetcher; -import com.mapbox.services.android.navigation.v5.routeprogress.ProgressChangeListener; import com.mapbox.services.android.navigation.v5.routeprogress.RouteProgress; import com.mapbox.services.android.navigation.v5.utils.DistanceFormatter; import com.mapbox.services.android.navigation.v5.utils.LocaleUtils; @@ -84,20 +84,26 @@ public class NavigationViewModel extends AndroidViewModel { private int timeFormatType; private boolean isRunning; private boolean isChangingConfigurations; + private MapConnectivityController connectivityController; + private MapOfflineManager mapOfflineManager; public NavigationViewModel(Application application) { super(application); this.accessToken = Mapbox.getAccessToken(); initializeLocationEngine(); initializeRouter(); - routeUtils = new RouteUtils(); - localeUtils = new LocaleUtils(); + this.routeUtils = new RouteUtils(); + this.localeUtils = new LocaleUtils(); + this.connectivityController = new MapConnectivityController(); } // Package private (no modifier) for testing purposes - NavigationViewModel(Application application, MapboxNavigation navigation) { + NavigationViewModel(Application application, MapboxNavigation navigation, + MapConnectivityController connectivityController, MapOfflineManager mapOfflineManager) { super(application); this.navigation = navigation; + this.connectivityController = connectivityController; + this.mapOfflineManager = mapOfflineManager; } // Package private (no modifier) for testing purposes @@ -115,8 +121,9 @@ public NavigationViewModel(Application application) { public void onDestroy(boolean isChangingConfigurations) { this.isChangingConfigurations = isChangingConfigurations; if (!isChangingConfigurations) { - router.onDestroy(); + destroyRouter(); endNavigation(); + destroyMapOffline(); deactivateInstructionPlayer(); isRunning = false; } @@ -201,6 +208,7 @@ void initialize(NavigationViewOptions options) { initializeVoiceInstructionLoader(); initializeVoiceInstructionCache(); initializeNavigationSpeechPlayer(options); + initializeMapOfflineManager(options); } router.extractRouteOptions(options); } @@ -248,6 +256,18 @@ void updateRoute(DirectionsRoute route) { resetConfigurationFlag(); } + void updateRouteProgress(RouteProgress routeProgress) { + this.routeProgress = routeProgress; + sendEventArrival(routeProgress); + instructionModel.setValue(new InstructionModel(distanceFormatter, routeProgress)); + summaryModel.setValue(new SummaryModel(getApplication(), distanceFormatter, routeProgress, timeFormatType)); + } + + void updateLocation(Location location) { + router.updateLocation(location); + navigationLocation.setValue(location); + } + void sendEventFailedReroute(String errorMessage) { if (navigationViewEventDispatcher != null) { navigationViewEventDispatcher.onFailedReroute(errorMessage); @@ -312,6 +332,25 @@ private void initializeNavigationSpeechPlayer(NavigationViewOptions options) { this.speechPlayer = new NavigationSpeechPlayer(speechPlayerProvider); } + private void initializeMapOfflineManager(NavigationViewOptions options) { + MapOfflineOptions mapOfflineOptions = options.offlineMapOptions(); + if (mapOfflineOptions == null) { + return; + } + String mapDatabasePath = mapOfflineOptions.getDatabasePath(); + String mapStyleUrl = mapOfflineOptions.getStyleUrl(); + Context applicationContext = getApplication().getApplicationContext(); + OfflineManager offlineManager = OfflineManager.getInstance(applicationContext); + float pixelRatio = applicationContext.getResources().getDisplayMetrics().density; + OfflineRegionDefinitionProvider definitionProvider = new OfflineRegionDefinitionProvider(mapStyleUrl, pixelRatio); + OfflineMetadataProvider metadataProvider = new OfflineMetadataProvider(); + RegionDownloadCallback regionDownloadCallback = new RegionDownloadCallback(connectivityController); + mapOfflineManager = new MapOfflineManager(offlineManager, definitionProvider, metadataProvider, + connectivityController, regionDownloadCallback); + NavigationOfflineDatabaseCallback callback = new NavigationOfflineDatabaseCallback(navigation, mapOfflineManager); + mapOfflineManager.loadDatabase(mapDatabasePath, callback); + } + private void initializeVoiceInstructionLoader() { Cache cache = new Cache(new File(getApplication().getCacheDir(), OKHTTP_INSTRUCTION_CACHE), TEN_MEGABYTE_CACHE_SIZE); @@ -341,7 +380,7 @@ private void initializeNavigation(Context context, MapboxNavigationOptions optio } private void addNavigationListeners() { - navigation.addProgressChangeListener(progressChangeListener); + navigation.addProgressChangeListener(new NavigationViewModelProgressChangeListener(this)); navigation.addOffRouteListener(offRouteListener); navigation.addMilestoneEventListener(milestoneEventListener); navigation.addNavigationEventListener(navigationEventListener); @@ -355,18 +394,6 @@ private void addMilestones(NavigationViewOptions options) { } } - private ProgressChangeListener progressChangeListener = new ProgressChangeListener() { - @Override - public void onProgressChange(Location location, RouteProgress routeProgress) { - NavigationViewModel.this.routeProgress = routeProgress; - router.updateLocation(location); - instructionModel.setValue(new InstructionModel(distanceFormatter, routeProgress)); - summaryModel.setValue(new SummaryModel(getApplication(), distanceFormatter, routeProgress, timeFormatType)); - navigationLocation.setValue(location); - sendEventArrival(routeProgress); - } - }; - private OffRouteListener offRouteListener = new OffRouteListener() { @Override public void userOffRoute(Location location) { @@ -417,6 +444,12 @@ private void updateReplayEngine(DirectionsRoute route) { } } + private void destroyRouter() { + if (router != null) { + router.onDestroy(); + } + } + private void endNavigation() { if (navigation != null) { navigation.onDestroy(); @@ -433,6 +466,13 @@ private void clearDynamicCameraMap() { } } + private void destroyMapOffline() { + if (mapOfflineManager != null) { + mapOfflineManager.onDestroy(); + } + connectivityController.assign(null); + } + private void deactivateInstructionPlayer() { if (speechPlayer != null) { speechPlayer.onDestroy(); diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModelProgressChangeListener.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModelProgressChangeListener.java new file mode 100644 index 00000000000..51b769f33cf --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModelProgressChangeListener.java @@ -0,0 +1,21 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import android.location.Location; + +import com.mapbox.services.android.navigation.v5.routeprogress.ProgressChangeListener; +import com.mapbox.services.android.navigation.v5.routeprogress.RouteProgress; + +class NavigationViewModelProgressChangeListener implements ProgressChangeListener { + + private final NavigationViewModel viewModel; + + NavigationViewModelProgressChangeListener(NavigationViewModel viewModel) { + this.viewModel = viewModel; + } + + @Override + public void onProgressChange(Location location, RouteProgress routeProgress) { + viewModel.updateRouteProgress(routeProgress); + viewModel.updateLocation(location); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewOptions.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewOptions.java index 63e34f8e006..c7c4b404b2b 100644 --- a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewOptions.java +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewOptions.java @@ -122,6 +122,14 @@ public abstract static class Builder { */ public abstract Builder offlineRoutingTilesVersion(String offlineVersion); + /** + * Add options to configure offline maps. + * + * @param mapOfflineOptions for offline configuration + * @return this builder + */ + public abstract Builder offlineMapOptions(MapOfflineOptions mapOfflineOptions); + public abstract NavigationViewOptions build(); } diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineDatabaseLoadedCallback.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineDatabaseLoadedCallback.java new file mode 100644 index 00000000000..dd10d781ee4 --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineDatabaseLoadedCallback.java @@ -0,0 +1,8 @@ +package com.mapbox.services.android.navigation.ui.v5; + +interface OfflineDatabaseLoadedCallback { + + void onComplete(); + + void onError(String error); +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineMetadataProvider.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineMetadataProvider.java new file mode 100644 index 00000000000..4c0941706c6 --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineMetadataProvider.java @@ -0,0 +1,23 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import android.support.annotation.Nullable; + +import org.json.JSONObject; + +class OfflineMetadataProvider { + + private static final String ROUTE_SUMMARY = "route_summary"; + private static final String JSON_CHARSET = "UTF-8"; + + @Nullable + byte[] buildMetadataFor(String routeSummary) { + try { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(ROUTE_SUMMARY, routeSummary); + String json = jsonObject.toString(); + return json.getBytes(JSON_CHARSET); + } catch (Exception exception) { + return null; + } + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRegionDefinitionProvider.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRegionDefinitionProvider.java new file mode 100644 index 00000000000..f028aea25d6 --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRegionDefinitionProvider.java @@ -0,0 +1,27 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import com.mapbox.geojson.Geometry; +import com.mapbox.mapboxsdk.offline.OfflineGeometryRegionDefinition; + +class OfflineRegionDefinitionProvider { + + private static final int MIN_ZOOM = 11; + private static final int MAX_ZOOM = 17; + private final String styleUrl; + private final float pixelRatio; + + OfflineRegionDefinitionProvider(String styleUrl, float pixelRatio) { + this.styleUrl = styleUrl; + this.pixelRatio = pixelRatio; + } + + OfflineGeometryRegionDefinition buildRegionFor(Geometry routeGeometry) { + return new OfflineGeometryRegionDefinition( + styleUrl, + routeGeometry, + MIN_ZOOM, + MAX_ZOOM, + pixelRatio + ); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRegionDownloadCallback.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRegionDownloadCallback.java new file mode 100644 index 00000000000..fd6d731217b --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRegionDownloadCallback.java @@ -0,0 +1,8 @@ +package com.mapbox.services.android.navigation.ui.v5; + +interface OfflineRegionDownloadCallback { + + void onComplete(); + + void onError(String error); +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRegionObserver.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRegionObserver.java new file mode 100644 index 00000000000..fb4f6d18cce --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRegionObserver.java @@ -0,0 +1,31 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import com.mapbox.mapboxsdk.offline.OfflineRegion; +import com.mapbox.mapboxsdk.offline.OfflineRegionError; +import com.mapbox.mapboxsdk.offline.OfflineRegionStatus; + +class OfflineRegionObserver implements OfflineRegion.OfflineRegionObserver { + + private final OfflineRegionDownloadCallback callback; + + OfflineRegionObserver(OfflineRegionDownloadCallback callback) { + this.callback = callback; + } + + @Override + public void onStatusChanged(OfflineRegionStatus status) { + if (status.isComplete()) { + callback.onComplete(); + } + } + + @Override + public void onError(OfflineRegionError error) { + callback.onError(String.format("%s %s", error.getMessage(), error.getReason())); + } + + @Override + public void mapboxTileCountLimitExceeded(long limit) { + callback.onError(String.format("Offline map tile limit reached %s", limit)); + } +} diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/RegionDownloadCallback.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/RegionDownloadCallback.java new file mode 100644 index 00000000000..c155d94cd97 --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/RegionDownloadCallback.java @@ -0,0 +1,21 @@ +package com.mapbox.services.android.navigation.ui.v5; + +class RegionDownloadCallback implements OfflineRegionDownloadCallback { + + private static final Boolean DISCONNECT_STATE = false; + private final MapConnectivityController connectivityController; + + RegionDownloadCallback(MapConnectivityController connectivityController) { + this.connectivityController = connectivityController; + } + + @Override + public void onComplete() { + connectivityController.assign(DISCONNECT_STATE); + } + + @Override + public void onError(String error) { + connectivityController.assign(DISCONNECT_STATE); + } +} diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/CreateOfflineRegionCallbackTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/CreateOfflineRegionCallbackTest.java new file mode 100644 index 00000000000..df174e5b5d7 --- /dev/null +++ b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/CreateOfflineRegionCallbackTest.java @@ -0,0 +1,49 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import com.mapbox.mapboxsdk.offline.OfflineRegion; + +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class CreateOfflineRegionCallbackTest { + + @Test + public void checksOfflineRegionDownloadStateSetToActiveWhenOnCreate() { + OfflineRegionDownloadCallback mockedOfflineRegionDownloadCallback = mock(OfflineRegionDownloadCallback.class); + OfflineRegion mockedOfflineRegion = mock(OfflineRegion.class); + CreateOfflineRegionCallback theOfflineRegionCallback = + new CreateOfflineRegionCallback(mockedOfflineRegionDownloadCallback); + + theOfflineRegionCallback.onCreate(mockedOfflineRegion); + + verify(mockedOfflineRegion).setDownloadState(eq(OfflineRegion.STATE_ACTIVE)); + } + + @Test + public void checksOfflineRegionObserverIsSetWhenOnCreate() { + OfflineRegionDownloadCallback mockedOfflineRegionDownloadCallback = mock(OfflineRegionDownloadCallback.class); + OfflineRegion mockedOfflineRegion = mock(OfflineRegion.class); + CreateOfflineRegionCallback theOfflineRegionCallback = + new CreateOfflineRegionCallback(mockedOfflineRegionDownloadCallback); + + theOfflineRegionCallback.onCreate(mockedOfflineRegion); + + verify(mockedOfflineRegion).setObserver(any(OfflineRegion.OfflineRegionObserver.class)); + } + + @Test + public void checksOnErrorCallbackIsCalledWhenOnError() { + OfflineRegionDownloadCallback mockedOfflineRegionDownloadCallback = mock(OfflineRegionDownloadCallback.class); + CreateOfflineRegionCallback theOfflineRegionCallback = + new CreateOfflineRegionCallback(mockedOfflineRegionDownloadCallback); + String anErrorMessage = "an error message"; + + theOfflineRegionCallback.onError(anErrorMessage); + + verify(mockedOfflineRegionDownloadCallback).onError(eq(anErrorMessage)); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/MapOfflineManagerTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/MapOfflineManagerTest.java new file mode 100644 index 00000000000..ff5170d8afa --- /dev/null +++ b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/MapOfflineManagerTest.java @@ -0,0 +1,192 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import android.location.Location; + +import com.mapbox.api.directions.v5.models.DirectionsRoute; +import com.mapbox.api.directions.v5.models.RouteOptions; +import com.mapbox.geojson.Geometry; +import com.mapbox.geojson.gson.GeometryGeoJson; +import com.mapbox.mapboxsdk.offline.OfflineGeometryRegionDefinition; +import com.mapbox.mapboxsdk.offline.OfflineManager; +import com.mapbox.services.android.navigation.v5.routeprogress.RouteProgress; + +import org.junit.Test; +import org.mockito.InOrder; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class MapOfflineManagerTest { + + @Test + public void checksDefaultMapConnectivityIsSetWhenDownloadingRouteBuffer() { + String aRouteSummary = "cjuykbm4705v26pnpvqlbjm5n"; + MapConnectivityController mockedMapConnectivityController = mock(MapConnectivityController.class); + MapOfflineManager theMapOfflineManager = buildMapOfflineManager(aRouteSummary, mockedMapConnectivityController); + Location mockedLocation = mock(Location.class); + Geometry aRouteBufferGeometry = buildARouteBufferGeometry(); + RouteProgress mockedRouteProgress = buildMockRouteProgress(aRouteSummary, aRouteBufferGeometry); + Boolean defaultState = null; + + theMapOfflineManager.onProgressChange(mockedLocation, mockedRouteProgress); + + verify(mockedMapConnectivityController).assign(eq(defaultState)); + } + + @Test + public void checksCreateOfflineRegionIsCalledWhenDownloadingRouteBuffer() { + OfflineManager mockedOfflineManager = mock(OfflineManager.class); + OfflineRegionDefinitionProvider mockedOfflineRegionDefinitionProvider = mock(OfflineRegionDefinitionProvider.class); + String guidanceStyleUrl = "mapbox://styles/mapbox/navigation-guidance-day-v4"; + float anyPixelRatio = 3.0f; + OfflineRegionDefinitionProvider anOfflineRegionDefinitionProvider = + new OfflineRegionDefinitionProvider(guidanceStyleUrl, anyPixelRatio); + Geometry aRouteBufferGeometry = buildARouteBufferGeometry(); + OfflineGeometryRegionDefinition routeBufferDefinition = + anOfflineRegionDefinitionProvider.buildRegionFor(aRouteBufferGeometry); + when(mockedOfflineRegionDefinitionProvider.buildRegionFor(eq(aRouteBufferGeometry))).thenReturn(routeBufferDefinition); + OfflineMetadataProvider mockedOfflineMetadataProvider = mock(OfflineMetadataProvider.class); + OfflineMetadataProvider anOfflineMetadataProvider = new OfflineMetadataProvider(); + String aRouteSummary = "cjuykbm4705v26pnpvqlbjm5n"; + byte[] routeSummaryMetadata = anOfflineMetadataProvider.buildMetadataFor(aRouteSummary); + when(mockedOfflineMetadataProvider.buildMetadataFor(eq(aRouteSummary))).thenReturn(routeSummaryMetadata); + MapConnectivityController mockedMapConnectivityController = mock(MapConnectivityController.class); + RegionDownloadCallback mockedRegionDownloadCallback = mock(RegionDownloadCallback.class); + MapOfflineManager theMapOfflineManager = new MapOfflineManager(mockedOfflineManager, + mockedOfflineRegionDefinitionProvider, mockedOfflineMetadataProvider, mockedMapConnectivityController, + mockedRegionDownloadCallback); + Location mockedLocation = mock(Location.class); + RouteProgress mockedRouteProgress = buildMockRouteProgress(aRouteSummary, aRouteBufferGeometry); + + theMapOfflineManager.onProgressChange(mockedLocation, mockedRouteProgress); + + verify(mockedOfflineManager).createOfflineRegion(eq(routeBufferDefinition), eq(routeSummaryMetadata), + any(CreateOfflineRegionCallback.class)); + } + + @Test + public void checksDownloadIfPreviousRouteGeometryIsNotNullAndIsNotEqualToCurrentRouteGeometry() { + OfflineManager mockedOfflineManager = mock(OfflineManager.class); + OfflineRegionDefinitionProvider mockedOfflineRegionDefinitionProvider = mock(OfflineRegionDefinitionProvider.class); + String guidanceStyleUrl = "mapbox://styles/mapbox/navigation-guidance-day-v4"; + float anyPixelRatio = 3.0f; + OfflineRegionDefinitionProvider anOfflineRegionDefinitionProvider = + new OfflineRegionDefinitionProvider(guidanceStyleUrl, anyPixelRatio); + OfflineMetadataProvider mockedOfflineMetadataProvider = mock(OfflineMetadataProvider.class); + OfflineMetadataProvider anOfflineMetadataProvider = new OfflineMetadataProvider(); + String aRouteSummary = "cjuykbm4705v26pnpvqlbjm5n"; + byte[] routeSummaryMetadata = anOfflineMetadataProvider.buildMetadataFor(aRouteSummary); + when(mockedOfflineMetadataProvider.buildMetadataFor(eq(aRouteSummary))).thenReturn(routeSummaryMetadata); + MapConnectivityController mockedMapConnectivityController = mock(MapConnectivityController.class); + RegionDownloadCallback mockedRegionDownloadCallback = mock(RegionDownloadCallback.class); + MapOfflineManager theMapOfflineManager = new MapOfflineManager(mockedOfflineManager, + mockedOfflineRegionDefinitionProvider, mockedOfflineMetadataProvider, mockedMapConnectivityController, + mockedRegionDownloadCallback); + Location mockedLocation = mock(Location.class); + Geometry aRouteBufferGeometry = buildARouteBufferGeometry(); + RouteProgress mockedRouteProgress = buildMockRouteProgress(aRouteSummary, aRouteBufferGeometry); + Geometry anotherRouteBufferGeometry = buildAnotherRouteBufferGeometry(); + when(mockedRouteProgress.routeGeometryWithBuffer()).thenReturn(aRouteBufferGeometry, anotherRouteBufferGeometry); + OfflineGeometryRegionDefinition aRouteBufferDefinition = + anOfflineRegionDefinitionProvider.buildRegionFor(aRouteBufferGeometry); + OfflineGeometryRegionDefinition anotherRouteBufferDefinition = + anOfflineRegionDefinitionProvider.buildRegionFor(anotherRouteBufferGeometry); + when(mockedOfflineRegionDefinitionProvider.buildRegionFor(any(Geometry.class))) + .thenReturn(aRouteBufferDefinition, anotherRouteBufferDefinition); + Boolean defaultState = null; + theMapOfflineManager.onProgressChange(mockedLocation, mockedRouteProgress); + + theMapOfflineManager.onProgressChange(mockedLocation, mockedRouteProgress); + + verify(mockedMapConnectivityController, times(2)).assign(eq(defaultState)); + InOrder inOrder = inOrder(mockedOfflineManager, mockedOfflineManager); + inOrder.verify(mockedOfflineManager).createOfflineRegion(eq(aRouteBufferDefinition), eq(routeSummaryMetadata), + any(CreateOfflineRegionCallback.class)); + inOrder.verify(mockedOfflineManager).createOfflineRegion(eq(anotherRouteBufferDefinition), eq(routeSummaryMetadata), + any(CreateOfflineRegionCallback.class)); + } + + @Test + public void checksMergeOfflineRegionsIsCalledWhenLoadDatabase() { + OfflineManager mockedOfflineManager = mock(OfflineManager.class); + OfflineRegionDefinitionProvider mockedOfflineRegionDefinitionProvider = mock(OfflineRegionDefinitionProvider.class); + OfflineMetadataProvider mockedOfflineMetadataProvider = mock(OfflineMetadataProvider.class); + MapConnectivityController mockedMapConnectivityController = mock(MapConnectivityController.class); + RegionDownloadCallback mockedRegionDownloadCallback = mock(RegionDownloadCallback.class); + MapOfflineManager theMapOfflineManager = new MapOfflineManager(mockedOfflineManager, + mockedOfflineRegionDefinitionProvider, mockedOfflineMetadataProvider, mockedMapConnectivityController, + mockedRegionDownloadCallback); + String aDatabasePath = "a/database/path"; + OfflineDatabaseLoadedCallback mockedOfflineDatabaseLoadedCallback = mock(OfflineDatabaseLoadedCallback.class); + + theMapOfflineManager.loadDatabase(aDatabasePath, mockedOfflineDatabaseLoadedCallback); + + verify(mockedOfflineManager).mergeOfflineRegions(eq(aDatabasePath), any(MergeOfflineRegionsCallback.class)); + } + + @Test + public void checksMergeOfflineRegionsCallbackOnDestroyIsCalledIfNotNullWhenOnDestroy() { + OfflineManager mockedOfflineManager = mock(OfflineManager.class); + OfflineRegionDefinitionProvider mockedOfflineRegionDefinitionProvider = mock(OfflineRegionDefinitionProvider.class); + OfflineMetadataProvider mockedOfflineMetadataProvider = mock(OfflineMetadataProvider.class); + MapConnectivityController mockedMapConnectivityController = mock(MapConnectivityController.class); + RegionDownloadCallback mockedRegionDownloadCallback = mock(RegionDownloadCallback.class); + MergeOfflineRegionsCallback mockedMergeOfflineRegionsCallback = mock(MergeOfflineRegionsCallback.class); + MapOfflineManager theMapOfflineManager = new MapOfflineManager(mockedOfflineManager, + mockedOfflineRegionDefinitionProvider, mockedOfflineMetadataProvider, mockedMapConnectivityController, + mockedRegionDownloadCallback, mockedMergeOfflineRegionsCallback); + + theMapOfflineManager.onDestroy(); + + verify(mockedMergeOfflineRegionsCallback).onDestroy(); + } + + private MapOfflineManager buildMapOfflineManager(String routeSummary, + MapConnectivityController mapConnectivityController) { + OfflineManager mockedOfflineManager = mock(OfflineManager.class); + OfflineRegionDefinitionProvider mockedOfflineRegionDefinitionProvider = mock(OfflineRegionDefinitionProvider.class); + OfflineMetadataProvider mockedOfflineMetadataProvider = mock(OfflineMetadataProvider.class); + OfflineMetadataProvider anOfflineMetadataProvider = new OfflineMetadataProvider(); + byte[] routeSummaryMetadata = anOfflineMetadataProvider.buildMetadataFor(routeSummary); + when(mockedOfflineMetadataProvider.buildMetadataFor(eq(routeSummary))).thenReturn(routeSummaryMetadata); + RegionDownloadCallback mockedRegionDownloadCallback = mock(RegionDownloadCallback.class); + return new MapOfflineManager(mockedOfflineManager, mockedOfflineRegionDefinitionProvider, + mockedOfflineMetadataProvider, mapConnectivityController, mockedRegionDownloadCallback); + } + + private RouteProgress buildMockRouteProgress(String routeSummary, Geometry routeBufferGeometry) { + RouteProgress mockedRouteProgress = mock(RouteProgress.class); + when(mockedRouteProgress.routeGeometryWithBuffer()).thenReturn(routeBufferGeometry); + DirectionsRoute mockedRoute = mock(DirectionsRoute.class); + RouteOptions mockedRouteOptions = mock(RouteOptions.class); + when(mockedRouteOptions.requestUuid()).thenReturn(routeSummary); + when(mockedRoute.routeOptions()).thenReturn(mockedRouteOptions); + when(mockedRouteProgress.directionsRoute()).thenReturn(mockedRoute); + return mockedRouteProgress; + } + + private Geometry buildARouteBufferGeometry() { + return GeometryGeoJson.fromJson("{\"type\":\"Polygon\",\"coordinates\":[[[-77" + + ".152533,39.085537],[-77.152533,39.083038],[-77.150031,39.083038],[-77.150031,39.085537],[-77.147529,39" + + ".085537],[-77.147529,39.088039],[-77.147529,39.090538],[-77.150031,39.090538],[-77.150031,39.093037],[-77" + + ".150031,39.095539],[-77.150031,39.098038],[-77.150031,39.100540],[-77.150031,39.103039],[-77.152533,39" + + ".103039],[-77.152533,39.105537],[-77.155028,39.105537],[-77.155028,39.108040],[-77.155028,39.110538],[-77" + + ".157531,39.110538],[-77.157531,39.113037],[-77.160033,39.113037],[-77.160033,39.115536],[-77.162528,39" + + ".115540],[-77.162528,39.118038],[-77.165030,39.118038],[-77.165030,39.115536],[-77.167533,39.115536],[-77" + + ".167533,39.113037],[-77.167533,39.110538],[-77.165030,39.110538],[-77.165030,39.108040],[-77.162536,39" + + ".108036],[-77.162536,39.105537],[-77.162536,39.103039],[-77.160033,39.103039],[-77.160033,39.100540],[-77" + + ".157531,39.100536],[-77.157531,39.098038],[-77.157531,39.095535],[-77.157531,39.093037],[-77.157531,39" + + ".090538],[-77.157531,39.088039],[-77.155036,39.088036],[-77.155036,39.085537],[-77.152533,39.085537]]]}"); + } + + private Geometry buildAnotherRouteBufferGeometry() { + return GeometryGeoJson.fromJson("{\"type\":\"Polygon\",\"coordinates\":[[[-77" + + ".152533,39.085537],[-77.152533,39.083038],[-77.150031,39.083038],[-77.150031,39.085537],[-77.147529,39" + + ".085537],[-77.147529,39.088039],[-77.147529,39.090538]]]}"); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/MergeOfflineRegionsCallbackTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/MergeOfflineRegionsCallbackTest.java new file mode 100644 index 00000000000..2ed08ce2f54 --- /dev/null +++ b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/MergeOfflineRegionsCallbackTest.java @@ -0,0 +1,48 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import com.mapbox.mapboxsdk.offline.OfflineRegion; + +import org.junit.Test; + +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class MergeOfflineRegionsCallbackTest { + + @Test + public void checksOfflineDatabaseLoadedOnCompleteCallbackIsCalledWhenOnMerge() { + OfflineDatabaseLoadedCallback mockedOfflineDatabaseLoadedCallback = mock(OfflineDatabaseLoadedCallback.class); + MergeOfflineRegionsCallback theMergeOfflineRegionsCallback = + new MergeOfflineRegionsCallback(mockedOfflineDatabaseLoadedCallback); + OfflineRegion[] anyOfflineRegion = new OfflineRegion[0]; + + theMergeOfflineRegionsCallback.onMerge(anyOfflineRegion); + + verify(mockedOfflineDatabaseLoadedCallback).onComplete(); + } + + @Test + public void checksOfflineDatabaseLoadedOnErrorCallbackIsCalledWhenOnError() { + OfflineDatabaseLoadedCallback mockedOfflineDatabaseLoadedCallback = mock(OfflineDatabaseLoadedCallback.class); + MergeOfflineRegionsCallback theMergeOfflineRegionsCallback = + new MergeOfflineRegionsCallback(mockedOfflineDatabaseLoadedCallback); + String anyError = "any error message"; + + theMergeOfflineRegionsCallback.onError(anyError); + + verify(mockedOfflineDatabaseLoadedCallback).onError(eq(anyError)); + } + + @Test + public void checksOfflineDatabaseLoadedOnDestroyReleasesCallback() { + OfflineDatabaseLoadedCallback mockedOfflineDatabaseLoadedCallback = mock(OfflineDatabaseLoadedCallback.class); + MergeOfflineRegionsCallback theMergeOfflineRegionsCallback = + new MergeOfflineRegionsCallback(mockedOfflineDatabaseLoadedCallback); + + OfflineDatabaseLoadedCallback destroyedCallback = theMergeOfflineRegionsCallback.onDestroy(); + + assertNull(destroyedCallback); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationOfflineDatabaseCallbackTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationOfflineDatabaseCallbackTest.java new file mode 100644 index 00000000000..b2a35a0c5e0 --- /dev/null +++ b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationOfflineDatabaseCallbackTest.java @@ -0,0 +1,36 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import com.mapbox.services.android.navigation.v5.navigation.MapboxNavigation; + +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class NavigationOfflineDatabaseCallbackTest { + + @Test + public void checksMapOfflineManagerProgressChangeListenerIsAddedWhenOnComplete() { + MapboxNavigation mockedNavigation = mock(MapboxNavigation.class); + MapOfflineManager mockedMapOfflineManager = mock(MapOfflineManager.class); + NavigationOfflineDatabaseCallback theNavigationOfflineDatabaseCallback = + new NavigationOfflineDatabaseCallback(mockedNavigation, mockedMapOfflineManager); + + theNavigationOfflineDatabaseCallback.onComplete(); + + verify(mockedNavigation).addProgressChangeListener(eq(mockedMapOfflineManager)); + } + + @Test + public void checksMapOfflineManagerOnDestroyIsCalledWhenOnDestroy() { + MapboxNavigation mockedNavigation = mock(MapboxNavigation.class); + MapOfflineManager mockedMapOfflineManager = mock(MapOfflineManager.class); + NavigationOfflineDatabaseCallback theNavigationOfflineDatabaseCallback = + new NavigationOfflineDatabaseCallback(mockedNavigation, mockedMapOfflineManager); + + theNavigationOfflineDatabaseCallback.onDestroy(); + + verify(mockedMapOfflineManager).onDestroy(); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModelProgressChangeListenerTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModelProgressChangeListenerTest.java new file mode 100644 index 00000000000..a562521f573 --- /dev/null +++ b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModelProgressChangeListenerTest.java @@ -0,0 +1,40 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import android.location.Location; + +import com.mapbox.services.android.navigation.v5.routeprogress.RouteProgress; + +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class NavigationViewModelProgressChangeListenerTest { + + @Test + public void checksNavigationViewModelRouteProgressIsUpdatedWhenOnProgressChange() { + NavigationViewModel mockedNavigationViewModel = mock(NavigationViewModel.class); + NavigationViewModelProgressChangeListener theNavigationViewModelProgressChangeListener = + new NavigationViewModelProgressChangeListener(mockedNavigationViewModel); + Location anyLocation = mock(Location.class); + RouteProgress theRouteProgress = mock(RouteProgress.class); + + theNavigationViewModelProgressChangeListener.onProgressChange(anyLocation, theRouteProgress); + + verify(mockedNavigationViewModel).updateRouteProgress(eq(theRouteProgress)); + } + + @Test + public void checksNavigationViewModelLocationIsUpdatedWhenOnProgressChange() { + NavigationViewModel mockedNavigationViewModel = mock(NavigationViewModel.class); + NavigationViewModelProgressChangeListener theNavigationViewModelProgressChangeListener = + new NavigationViewModelProgressChangeListener(mockedNavigationViewModel); + Location theLocation = mock(Location.class); + RouteProgress anyRouteProgress = mock(RouteProgress.class); + + theNavigationViewModelProgressChangeListener.onProgressChange(theLocation, anyRouteProgress); + + verify(mockedNavigationViewModel).updateLocation(eq(theLocation)); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModelTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModelTest.java index d488898e460..9bee899994a 100644 --- a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModelTest.java +++ b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModelTest.java @@ -10,8 +10,9 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -24,7 +25,10 @@ public class NavigationViewModelTest { public void stopNavigation_progressListenersAreRemoved() { Application application = mock(Application.class); MapboxNavigation navigation = mock(MapboxNavigation.class); - NavigationViewModel viewModel = new NavigationViewModel(application, navigation); + MapConnectivityController mockedConnectivityController = mock(MapConnectivityController.class); + MapOfflineManager mapOfflineManager = mock(MapOfflineManager.class); + NavigationViewModel viewModel = new NavigationViewModel(application, navigation, mockedConnectivityController, + mapOfflineManager); viewModel.stopNavigation(); @@ -35,19 +39,54 @@ public void stopNavigation_progressListenersAreRemoved() { public void stopNavigation_milestoneListenersAreRemoved() { Application application = mock(Application.class); MapboxNavigation navigation = mock(MapboxNavigation.class); - NavigationViewModel viewModel = new NavigationViewModel(application, navigation); + MapConnectivityController mockedConnectivityController = mock(MapConnectivityController.class); + MapOfflineManager mapOfflineManager = mock(MapOfflineManager.class); + NavigationViewModel viewModel = new NavigationViewModel(application, navigation, mockedConnectivityController, + mapOfflineManager); viewModel.stopNavigation(); verify(navigation).removeMilestoneEventListener(null); } + @Test + public void stopNavigation_mapOfflineManagerOnDestroyIsCalledIfNotNull() { + Application application = mock(Application.class); + MapboxNavigation navigation = mock(MapboxNavigation.class); + MapConnectivityController mockedConnectivityController = mock(MapConnectivityController.class); + MapOfflineManager mapOfflineManager = mock(MapOfflineManager.class); + NavigationViewModel viewModel = new NavigationViewModel(application, navigation, mockedConnectivityController, + mapOfflineManager); + + viewModel.onDestroy(false); + + verify(mapOfflineManager).onDestroy(); + } + + @Test + public void stopNavigation_mapConnectivityControllerStateIsReset() { + Application application = mock(Application.class); + MapboxNavigation navigation = mock(MapboxNavigation.class); + MapConnectivityController mockedConnectivityController = mock(MapConnectivityController.class); + MapOfflineManager mapOfflineManager = mock(MapOfflineManager.class); + NavigationViewModel viewModel = new NavigationViewModel(application, navigation, mockedConnectivityController, + mapOfflineManager); + Boolean defaultState = null; + + viewModel.onDestroy(false); + + verify(mockedConnectivityController).assign(eq(defaultState)); + } + @Test public void updateRoute_navigationIsNotUpdatedWhenChangingConfigurations() { Application application = mock(Application.class); MapboxNavigation navigation = mock(MapboxNavigation.class); DirectionsRoute route = mock(DirectionsRoute.class); - NavigationViewModel viewModel = new NavigationViewModel(application, navigation); + MapConnectivityController mockedConnectivityController = mock(MapConnectivityController.class); + MapOfflineManager mapOfflineManager = mock(MapOfflineManager.class); + NavigationViewModel viewModel = new NavigationViewModel(application, navigation, mockedConnectivityController, + mapOfflineManager); viewModel.onDestroy(true); viewModel.updateRoute(route); diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineMetadataProviderTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineMetadataProviderTest.java new file mode 100644 index 00000000000..5cd60651781 --- /dev/null +++ b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineMetadataProviderTest.java @@ -0,0 +1,18 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class OfflineMetadataProviderTest { + + @Test + public void checksOfflineRouteMetadataCreation() { + String aRouteSummary = "cjuykbm4705v26pnpvqlbjm5n"; + OfflineMetadataProvider theOfflineMetadataProvider = new OfflineMetadataProvider(); + + byte[] routeSummaryMetadata = theOfflineMetadataProvider.buildMetadataFor(aRouteSummary); + + assertEquals("{\"route_summary\":\"cjuykbm4705v26pnpvqlbjm5n\"}", new String(routeSummaryMetadata)); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineRegionDefinitionProviderTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineRegionDefinitionProviderTest.java new file mode 100644 index 00000000000..2889eef4d34 --- /dev/null +++ b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineRegionDefinitionProviderTest.java @@ -0,0 +1,50 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import com.mapbox.geojson.Geometry; +import com.mapbox.mapboxsdk.offline.OfflineGeometryRegionDefinition; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +public class OfflineRegionDefinitionProviderTest { + + private static final double DELTA = 1E-10; + + @Test + public void buildRegionFor_geometryIsCorrectlySet() { + String styleUrl = "mapbox://style"; + float pixelRatio = 1f; + Geometry routeGeometry = mock(Geometry.class); + OfflineRegionDefinitionProvider provider = new OfflineRegionDefinitionProvider(styleUrl, pixelRatio); + + OfflineGeometryRegionDefinition offlineRegionDefinition = provider.buildRegionFor(routeGeometry); + + assertEquals(routeGeometry, offlineRegionDefinition.getGeometry()); + } + + @Test + public void buildRegionFor_styleUrlIsCorrectlySet() { + String styleUrl = "mapbox://style"; + float pixelRatio = 1f; + Geometry routeGeometry = mock(Geometry.class); + OfflineRegionDefinitionProvider provider = new OfflineRegionDefinitionProvider(styleUrl, pixelRatio); + + OfflineGeometryRegionDefinition offlineRegionDefinition = provider.buildRegionFor(routeGeometry); + + assertEquals(styleUrl, offlineRegionDefinition.getStyleURL()); + } + + @Test + public void buildRegionFor_pixelRatioIsCorrectlySet() { + String styleUrl = "mapbox://style"; + float pixelRatio = 1f; + Geometry routeGeometry = mock(Geometry.class); + OfflineRegionDefinitionProvider provider = new OfflineRegionDefinitionProvider(styleUrl, pixelRatio); + + OfflineGeometryRegionDefinition offlineRegionDefinition = provider.buildRegionFor(routeGeometry); + + assertEquals(pixelRatio, offlineRegionDefinition.getPixelRatio(), DELTA); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineRegionObserverTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineRegionObserverTest.java new file mode 100644 index 00000000000..b5176f8a11d --- /dev/null +++ b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineRegionObserverTest.java @@ -0,0 +1,49 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import com.mapbox.mapboxsdk.offline.OfflineRegionError; +import com.mapbox.mapboxsdk.offline.OfflineRegionStatus; + +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class OfflineRegionObserverTest { + + @Test + public void onStatusChanged_completeCallbackIsTriggeredWithCompleteStatus() { + OfflineRegionDownloadCallback callback = mock(OfflineRegionDownloadCallback.class); + OfflineRegionStatus status = mock(OfflineRegionStatus.class); + when(status.isComplete()).thenReturn(true); + OfflineRegionObserver offlineRegionObserver = new OfflineRegionObserver(callback); + + offlineRegionObserver.onStatusChanged(status); + + verify(callback).onComplete(); + } + + @Test + public void onError_errorCallbackIsTriggered() { + OfflineRegionDownloadCallback callback = mock(OfflineRegionDownloadCallback.class); + OfflineRegionError error = mock(OfflineRegionError.class); + when(error.getMessage()).thenReturn("an error occurred"); + when(error.getReason()).thenReturn("because xyz"); + OfflineRegionObserver offlineRegionObserver = new OfflineRegionObserver(callback); + + offlineRegionObserver.onError(error); + + verify(callback).onError(eq("an error occurred because xyz")); + } + + @Test + public void mapboxTileCountLimitExceeded_errorCallbackIsTriggered() { + OfflineRegionDownloadCallback callback = mock(OfflineRegionDownloadCallback.class); + OfflineRegionObserver offlineRegionObserver = new OfflineRegionObserver(callback); + + offlineRegionObserver.mapboxTileCountLimitExceeded(6000L); + + verify(callback).onError(eq("Offline map tile limit reached 6000")); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/RegionDownloadCallbackTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/RegionDownloadCallbackTest.java new file mode 100644 index 00000000000..f23c4f355cc --- /dev/null +++ b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/RegionDownloadCallbackTest.java @@ -0,0 +1,32 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import org.junit.Test; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class RegionDownloadCallbackTest { + + @Test + public void onComplete_disconnectStateIsAssigned() { + MapConnectivityController mapConnectivityController = mock(MapConnectivityController.class); + RegionDownloadCallback callback = new RegionDownloadCallback(mapConnectivityController); + + callback.onComplete(); + + verify(mapConnectivityController).assign(eq(false)); + } + + @Test + public void onError_disconnectStateIsAssigned() { + MapConnectivityController mapConnectivityController = mock(MapConnectivityController.class); + RegionDownloadCallback callback = new RegionDownloadCallback(mapConnectivityController); + + callback.onError("some error message"); + + verify(mapConnectivityController).assign(eq(false)); + + } +} \ No newline at end of file diff --git a/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/navigation/MapboxNavigator.java b/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/navigation/MapboxNavigator.java index a40419db5ba..0ec9c59ec8f 100644 --- a/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/navigation/MapboxNavigator.java +++ b/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/navigation/MapboxNavigator.java @@ -2,20 +2,27 @@ import android.location.Location; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import com.mapbox.api.directions.v5.models.DirectionsRoute; +import com.mapbox.geojson.Geometry; +import com.mapbox.geojson.LineString; import com.mapbox.geojson.Point; +import com.mapbox.geojson.gson.GeometryGeoJson; import com.mapbox.navigator.BannerInstruction; import com.mapbox.navigator.FixLocation; import com.mapbox.navigator.NavigationStatus; import com.mapbox.navigator.Navigator; import com.mapbox.navigator.VoiceInstruction; +import java.util.ArrayList; import java.util.Date; class MapboxNavigator { private static final int INDEX_FIRST_ROUTE = 0; + private static final float GRID_SIZE = 0.0025f; + private static final short BUFFER_DILATION = 1; private final Navigator navigator; private final RouteHandler routeHandler; @@ -89,6 +96,24 @@ synchronized BannerInstruction retrieveBannerInstruction(int index) { return navigator.getBannerInstruction(index); } + @Nullable + synchronized Geometry retrieveRouteGeometry() { + ArrayList routeGeometry = navigator.getRouteGeometry(); + if (routeGeometry == null) { + return null; + } + return LineString.fromLngLats(routeGeometry); + } + + @Nullable + synchronized Geometry retrieveRouteGeometryWithBuffer() { + String routeGeometryWithBuffer = navigator.getRouteBufferGeoJson(GRID_SIZE, BUFFER_DILATION); + if (routeGeometryWithBuffer == null) { + return null; + } + return GeometryGeoJson.fromJson(routeGeometryWithBuffer); + } + private FixLocation buildFixLocationFromLocation(Location location) { Date time = new Date(); Point rawPoint = Point.fromLngLat(location.getLongitude(), location.getLatitude()); diff --git a/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/navigation/NavigationConstants.java b/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/navigation/NavigationConstants.java index e728e0aacce..315271a91ae 100644 --- a/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/navigation/NavigationConstants.java +++ b/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/navigation/NavigationConstants.java @@ -150,9 +150,10 @@ private NavigationConstants() { // Bundle variable keys public static final String NAVIGATION_VIEW_ROUTE_KEY = "route_json"; public static final String NAVIGATION_VIEW_SIMULATE_ROUTE = "navigation_view_simulate_route"; - public static final String NAVIGATION_VIEW_ROUTE_PROFILE_KEY = "navigation_view_route_profile"; public static final String OFFLINE_PATH_KEY = "offline_path_key"; public static final String OFFLINE_VERSION_KEY = "offline_version_key"; + public static final String MAP_DATABASE_PATH_KEY = "offline_map_database_path_key"; + public static final String MAP_STYLE_URL_KEY = "offline_map_style_url_key"; // Step Maneuver Types public static final String STEP_MANEUVER_TYPE_TURN = "turn"; diff --git a/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/navigation/NavigationRouteProcessor.java b/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/navigation/NavigationRouteProcessor.java index 9e23f44fa16..70551da289a 100644 --- a/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/navigation/NavigationRouteProcessor.java +++ b/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/navigation/NavigationRouteProcessor.java @@ -7,6 +7,7 @@ import com.mapbox.api.directions.v5.models.LegStep; import com.mapbox.api.directions.v5.models.RouteLeg; import com.mapbox.api.directions.v5.models.StepIntersection; +import com.mapbox.geojson.Geometry; import com.mapbox.geojson.Point; import com.mapbox.navigator.BannerInstruction; import com.mapbox.navigator.NavigationStatus; @@ -44,10 +45,12 @@ class NavigationRouteProcessor { private List currentIntersections; private List> currentIntersectionDistances; private CurrentLegAnnotation currentLegAnnotation; + private Geometry routeGeometry; + private Geometry routeGeometryWithBuffer; RouteProgress buildNewRouteProgress(MapboxNavigator navigator, NavigationStatus status, DirectionsRoute route) { previousStatus = status; - updateRoute(route); + updateRoute(route, navigator); return buildRouteProgressFrom(status, navigator); } @@ -65,9 +68,11 @@ NavigationStatus retrievePreviousStatus() { return previousStatus; } - private void updateRoute(DirectionsRoute route) { + private void updateRoute(DirectionsRoute route, MapboxNavigator navigator) { if (this.route == null || !this.route.equals(route)) { this.route = route; + routeGeometry = navigator.retrieveRouteGeometry(); + routeGeometryWithBuffer = navigator.retrieveRouteGeometryWithBuffer(); } } @@ -114,6 +119,7 @@ private RouteProgress buildRouteProgressFrom(NavigationStatus status, MapboxNavi .inTunnel(status.getInTunnel()) .currentState(currentRouteState); + addRouteGeometries(progressBuilder); addVoiceInstructions(status, progressBuilder); addBannerInstructions(status, navigator, progressBuilder); addUpcomingStepPoints(progressBuilder); @@ -148,6 +154,11 @@ private void addUpcomingStepPoints(RouteProgress.Builder progressBuilder) { } } + private void addRouteGeometries(RouteProgress.Builder progressBuilder) { + progressBuilder.routeGeometry(routeGeometry); + progressBuilder.routeGeometryWithBuffer(routeGeometryWithBuffer); + } + private void addVoiceInstructions(NavigationStatus status, RouteProgress.Builder progressBuilder) { VoiceInstruction voiceInstruction = status.getVoiceInstruction(); progressBuilder.voiceInstruction(voiceInstruction); diff --git a/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/navigation/OnOfflineTilesRemovedCallback.java b/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/navigation/OnOfflineTilesRemovedCallback.java index 71fdc879cd5..dc8237ff372 100644 --- a/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/navigation/OnOfflineTilesRemovedCallback.java +++ b/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/navigation/OnOfflineTilesRemovedCallback.java @@ -4,7 +4,7 @@ /** * Listener that needs to be added to - * {@link MapboxOfflineRouter#removeTiles(BoundingBox, OnOfflineTilesRemovedCallback)} to know when the routing + * {@link MapboxOfflineRouter#removeTiles(String, BoundingBox, OnOfflineTilesRemovedCallback)} to know when the routing * tiles within the provided {@link BoundingBox} have been removed */ public interface OnOfflineTilesRemovedCallback { diff --git a/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/routeprogress/RouteProgress.java b/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/routeprogress/RouteProgress.java index a7658d481f6..baff0c9dadd 100644 --- a/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/routeprogress/RouteProgress.java +++ b/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/routeprogress/RouteProgress.java @@ -9,6 +9,7 @@ import com.mapbox.api.directions.v5.models.LegStep; import com.mapbox.api.directions.v5.models.RouteLeg; import com.mapbox.api.directions.v5.models.StepIntersection; +import com.mapbox.geojson.Geometry; import com.mapbox.geojson.Point; import com.mapbox.navigator.BannerInstruction; import com.mapbox.navigator.VoiceInstruction; @@ -188,6 +189,26 @@ public int remainingWaypoints() { @Nullable public abstract RouteProgressState currentState(); + /** + * Returns the current {@link DirectionsRoute} geometry. + * + * @return current route geometry + */ + @Nullable + public abstract Geometry routeGeometry(); + + /** + * Returns the current {@link DirectionsRoute} geometry with a buffer + * that encompasses visible tile surface are while navigating. + *

+ * This {@link Geometry} is ideal for offline downloads of map or routing tile + * data. + * + * @return current route geometry with buffer + */ + @Nullable + public abstract Geometry routeGeometryWithBuffer(); + public abstract RouteProgress.Builder toBuilder(); abstract LegStep currentStep(); @@ -285,6 +306,10 @@ public abstract Builder intersectionDistancesAlongStep( public abstract Builder currentState(@Nullable RouteProgressState currentState); + public abstract Builder routeGeometry(@Nullable Geometry routeGeometry); + + public abstract Builder routeGeometryWithBuffer(@Nullable Geometry routeGeometryWithBuffer); + abstract RouteProgress autoBuild(); // not public public RouteProgress build() {