From 17ad5a98552fecad3e8ae6e524cc6f536180c259 Mon Sep 17 00:00:00 2001 From: Tomasz Rybakiewicz Date: Fri, 13 Jan 2023 16:48:44 -0500 Subject: [PATCH 1/8] Android Auto - added support for Junction Views --- android-auto-app/build.gradle | 10 ++ android-auto-app/src/main/AndroidManifest.xml | 1 + .../androidauto/app/DrawerActivity.kt | 98 +++++++++++++++ .../examples/androidauto/app/MainActivity.kt | 117 ++++++++++++++++-- .../androidauto/utils/MapboxNavigationEx.kt | 62 ++++++++++ .../utils/NavigationViewController.kt | 73 +++++++++++ .../examples/androidauto/utils/TestRoutes.kt | 69 +++++++++++ .../main/res/drawable/ic_baseline_menu_24.xml | 10 ++ .../src/main/res/drawable/menu_button_bg.xml | 12 ++ .../src/main/res/layout/activity_drawer.xml | 46 +++++++ .../src/main/res/layout/activity_main.xml | 33 ++--- .../layout/layout_drawer_menu_nav_view.xml | 78 ++++++++++++ .../mapbox_activity_navigation_view.xml | 9 -- .../src/main/res/values/strings.xml | 10 ++ .../navigation/CarNavigationInfoMapper.kt | 16 +++ .../navigation/CarNavigationInfoProvider.kt | 32 ++++- .../navigation/CarNavigationInfoServices.kt | 28 +++++ .../CarNavigationInfoProviderTest.kt | 5 + 18 files changed, 670 insertions(+), 39 deletions(-) create mode 100644 android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/DrawerActivity.kt create mode 100644 android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/utils/MapboxNavigationEx.kt create mode 100644 android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/utils/NavigationViewController.kt create mode 100644 android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/utils/TestRoutes.kt create mode 100644 android-auto-app/src/main/res/drawable/ic_baseline_menu_24.xml create mode 100644 android-auto-app/src/main/res/drawable/menu_button_bg.xml create mode 100644 android-auto-app/src/main/res/layout/activity_drawer.xml create mode 100644 android-auto-app/src/main/res/layout/layout_drawer_menu_nav_view.xml delete mode 100644 android-auto-app/src/main/res/layout/mapbox_activity_navigation_view.xml diff --git a/android-auto-app/build.gradle b/android-auto-app/build.gradle index 750b69f81e9..de9f8eb0a77 100644 --- a/android-auto-app/build.gradle +++ b/android-auto-app/build.gradle @@ -75,6 +75,16 @@ dependencies { implementation("com.mapbox.navigation:ui-dropin:2.10.0-rc.1") implementation("com.mapbox.search:mapbox-search-android:1.0.0-beta.42") + // Support libraries + implementation dependenciesList.androidXCore + implementation dependenciesList.materialDesign + implementation dependenciesList.androidXAppCompat + implementation dependenciesList.androidXCardView + implementation dependenciesList.androidXConstraintLayout + implementation dependenciesList.androidXFragment + implementation dependenciesList.androidXLifecycleLivedata + implementation dependenciesList.androidXLifecycleRuntime + // Dependencies needed for this example. implementation dependenciesList.androidXAppCompat } \ No newline at end of file diff --git a/android-auto-app/src/main/AndroidManifest.xml b/android-auto-app/src/main/AndroidManifest.xml index bfe1928fa22..594e14bc6bd 100644 --- a/android-auto-app/src/main/AndroidManifest.xml +++ b/android-auto-app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:theme="@style/Theme.MapboxNavigationExamples"> diff --git a/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/DrawerActivity.kt b/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/DrawerActivity.kt new file mode 100644 index 00000000000..1c7b3b0136c --- /dev/null +++ b/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/DrawerActivity.kt @@ -0,0 +1,98 @@ +package com.mapbox.navigation.examples.androidauto.app + +import android.os.Bundle +import android.view.View +import android.widget.AdapterView +import android.widget.SpinnerAdapter +import androidx.annotation.CallSuper +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.AppCompatSpinner +import androidx.appcompat.widget.SwitchCompat +import androidx.core.view.GravityCompat +import androidx.lifecycle.MutableLiveData +import com.mapbox.navigation.examples.androidauto.databinding.ActivityDrawerBinding + +abstract class DrawerActivity : AppCompatActivity() { + + private lateinit var binding: ActivityDrawerBinding + + @CallSuper + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityDrawerBinding.inflate(layoutInflater) + binding.drawerContent.addView(onCreateContentView(), 0) + binding.drawerMenuContent.addView(onCreateMenuView()) + setContentView(binding.root) + + binding.menuButton.setOnClickListener { openDrawer() } + } + + abstract fun onCreateContentView(): View + + abstract fun onCreateMenuView(): View + + fun openDrawer() { + binding.drawerLayout.openDrawer(GravityCompat.START) + } + + fun closeDrawers() { + binding.drawerLayout.closeDrawers() + } + + protected fun bindSwitch( + switch: SwitchCompat, + getValue: () -> Boolean, + setValue: (v: Boolean) -> Unit + ) { + switch.isChecked = getValue() + switch.setOnCheckedChangeListener { _, isChecked -> setValue(isChecked) } + } + + protected fun bindSwitch( + switch: SwitchCompat, + liveData: MutableLiveData, + onChange: (value: Boolean) -> Unit + ) { + liveData.observe(this) { + switch.isChecked = it + onChange(it) + } + switch.setOnCheckedChangeListener { _, isChecked -> + liveData.value = isChecked + } + } + + protected fun bindSpinner( + spinner: AppCompatSpinner, + liveData: MutableLiveData, + onChange: (value: String) -> Unit + ) { + liveData.observe(this) { + if (spinner.selectedItem != it) { + spinner.setSelection(spinner.adapter.findItemPosition(it) ?: 0) + } + onChange(it) + } + + spinner.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>, + view: View?, + position: Int, + id: Long + ) { + liveData.value = parent.getItemAtPosition(position) as? String + } + + override fun onNothingSelected(parent: AdapterView<*>?) = Unit + } + } + + private fun SpinnerAdapter.findItemPosition(item: Any): Int? { + for (pos in 0..count) { + if (item == getItem(pos)) return pos + } + return null + } +} diff --git a/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/MainActivity.kt b/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/MainActivity.kt index 959fa2c2058..ddfa9ae9583 100644 --- a/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/MainActivity.kt +++ b/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/MainActivity.kt @@ -1,23 +1,118 @@ - package com.mapbox.navigation.examples.androidauto.app import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity +import android.view.View +import androidx.appcompat.widget.AppCompatImageView +import androidx.lifecycle.lifecycleScope +import com.mapbox.api.directions.v5.models.BannerInstructions +import com.mapbox.common.LogConfiguration +import com.mapbox.common.LoggingLevel +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI +import com.mapbox.navigation.core.MapboxNavigation +import com.mapbox.navigation.core.internal.extensions.flowRoutesUpdated +import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp +import com.mapbox.navigation.core.trip.session.BannerInstructionsObserver +import com.mapbox.navigation.dropin.navigationview.NavigationViewListener import com.mapbox.navigation.examples.androidauto.CarAppSyncComponent -import com.mapbox.navigation.examples.androidauto.databinding.MapboxActivityNavigationViewBinding +import com.mapbox.navigation.examples.androidauto.databinding.ActivityMainBinding +import com.mapbox.navigation.examples.androidauto.databinding.LayoutDrawerMenuNavViewBinding +import com.mapbox.navigation.examples.androidauto.utils.NavigationViewController +import com.mapbox.navigation.examples.androidauto.utils.TestRoutes +import com.mapbox.navigation.ui.base.installer.installComponents +import com.mapbox.navigation.ui.base.lifecycle.UIComponent +import com.mapbox.navigation.ui.maps.guidance.junction.api.MapboxJunctionApi +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch + +class MainActivity : DrawerActivity() { + + private lateinit var binding: ActivityMainBinding + private lateinit var menuBinding: LayoutDrawerMenuNavViewBinding -class MainActivity : AppCompatActivity() { - private lateinit var binding: MapboxActivityNavigationViewBinding + override fun onCreateContentView(): View { + binding = ActivityMainBinding.inflate(layoutInflater) + CarAppSyncComponent.getInstance().attachNavigationView(binding.navigationView) + return binding.root + } + override fun onCreateMenuView(): View { + menuBinding = LayoutDrawerMenuNavViewBinding.inflate(layoutInflater) + return menuBinding.root + } + + private lateinit var controller: NavigationViewController + + @OptIn(ExperimentalPreviewMapboxNavigationAPI::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = MapboxActivityNavigationViewBinding.inflate(layoutInflater) - setContentView(binding.root) - // TODO going to expose a public api to share a replay controller - // This allows to simulate your location -// binding.navigationView.api.routeReplayEnabled(true) + LogConfiguration.setLoggingLevel("nav-sdk", LoggingLevel.DEBUG) - CarAppSyncComponent.getInstance().attachNavigationView(binding.navigationView) + controller = NavigationViewController(this, binding.navigationView) + + menuBinding.toggleReplay.isChecked = binding.navigationView.api.isReplayEnabled() + menuBinding.toggleReplay.setOnCheckedChangeListener { _, isChecked -> + binding.navigationView.api.routeReplayEnabled(isChecked) + } + + menuBinding.junctionViewTestButton.setOnClickListener { + lifecycleScope.launch { + val (or, de) = TestRoutes.valueOf( + menuBinding.spinnerTestRoute.selectedItem as String + ) + controller.startActiveGuidance(or, de) + closeDrawers() + } + } + + MapboxNavigationApp.installComponents(this) { + component(Junctions(binding.junctionImageView)) + } + } + + /** + * Simple component for detecting and rendering Junction Views. + */ + private class Junctions( + private val imageView: AppCompatImageView + ) : UIComponent() { + private var junctionApi: MapboxJunctionApi? = null + + override fun onAttached(mapboxNavigation: MapboxNavigation) { + super.onAttached(mapboxNavigation) + val token = mapboxNavigation.navigationOptions.accessToken!! + junctionApi = MapboxJunctionApi(token) + + mapboxNavigation.flowBannerInstructions().observe { instructions -> + junctionApi?.generateJunction(instructions) { result -> + result.fold( + { imageView.setImageBitmap(null) }, + { imageView.setImageBitmap(it.bitmap) } + ) + } + } + mapboxNavigation.flowRoutesUpdated().observe { + if (it.navigationRoutes.isEmpty()) { + imageView.setImageBitmap(null) + } + } + } + + override fun onDetached(mapboxNavigation: MapboxNavigation) { + super.onDetached(mapboxNavigation) + junctionApi?.cancelAll() + junctionApi = null + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun MapboxNavigation.flowBannerInstructions(): Flow = + callbackFlow { + val observer = BannerInstructionsObserver { trySend(it) } + registerBannerInstructionsObserver(observer) + awaitClose { unregisterBannerInstructionsObserver(observer) } + } } } diff --git a/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/utils/MapboxNavigationEx.kt b/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/utils/MapboxNavigationEx.kt new file mode 100644 index 00000000000..6f75a30823d --- /dev/null +++ b/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/utils/MapboxNavigationEx.kt @@ -0,0 +1,62 @@ +package com.mapbox.navigation.examples.androidauto.utils + +import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.geojson.Point +import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions +import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions +import com.mapbox.navigation.base.route.NavigationRoute +import com.mapbox.navigation.base.route.NavigationRouterCallback +import com.mapbox.navigation.base.route.RouterFailure +import com.mapbox.navigation.base.route.RouterOrigin +import com.mapbox.navigation.core.MapboxNavigation +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +internal suspend fun MapboxNavigation.fetchRoute( + origin: Point, + destination: Point, +): List = + fetchRoute( + RouteOptions.builder() + .applyDefaultNavigationOptions() + .applyLanguageAndVoiceUnitOptions(navigationOptions.applicationContext) + .layersList(listOf(getZLevel(), null)) + .coordinatesList(listOf(origin, destination)) + .alternatives(true) + .build() + ) + +internal suspend fun MapboxNavigation.fetchRoute( + routeOptions: RouteOptions +): List = suspendCancellableCoroutine { cont -> + val requestId = requestRoutes( + routeOptions, + object : NavigationRouterCallback { + override fun onRoutesReady( + routes: List, + routerOrigin: RouterOrigin + ) { + cont.resume(routes) + } + + override fun onFailure( + reasons: List, + routeOptions: RouteOptions + ) { + cont.resumeWithException(FetchRouteError(reasons, routeOptions)) + } + + override fun onCanceled( + routeOptions: RouteOptions, + routerOrigin: RouterOrigin + ) = Unit + } + ) + cont.invokeOnCancellation { cancelRouteRequest(requestId) } +} + +internal class FetchRouteError( + val reasons: List, + val routeOptions: RouteOptions +) : Error() diff --git a/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/utils/NavigationViewController.kt b/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/utils/NavigationViewController.kt new file mode 100644 index 00000000000..c08c18a3c83 --- /dev/null +++ b/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/utils/NavigationViewController.kt @@ -0,0 +1,73 @@ +package com.mapbox.navigation.examples.androidauto.utils + +import android.location.Location +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.mapbox.geojson.Point +import com.mapbox.navigation.base.route.NavigationRoute +import com.mapbox.navigation.core.MapboxNavigation +import com.mapbox.navigation.core.internal.extensions.flowNewRawLocation +import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp +import com.mapbox.navigation.dropin.NavigationView +import com.mapbox.navigation.ui.base.lifecycle.UIComponent +import com.mapbox.navigation.utils.internal.toPoint +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first + +/** + * Lifecycle aware thin wrapper around NavigationView that offers convenience methods for + * fetching routes and starting active navigation. + */ +internal class NavigationViewController( + lifecycleOwner: LifecycleOwner, + private val navigationView: NavigationView +) : DefaultLifecycleObserver, UIComponent() { + init { + lifecycleOwner.lifecycle.addObserver(this) + } + + val location = MutableStateFlow(null) + private val mapboxNavigation = MutableStateFlow(null) + + override fun onCreate(owner: LifecycleOwner) { + MapboxNavigationApp.registerObserver(this) + } + + override fun onDestroy(owner: LifecycleOwner) { + MapboxNavigationApp.unregisterObserver(this) + } + + override fun onAttached(mapboxNavigation: MapboxNavigation) { + super.onAttached(mapboxNavigation) + this.mapboxNavigation.value = mapboxNavigation + mapboxNavigation.flowNewRawLocation().observe { + location.value = it + } + } + + override fun onDetached(mapboxNavigation: MapboxNavigation) { + super.onDetached(mapboxNavigation) + this.mapboxNavigation.value = null + } + + suspend fun startActiveGuidance(destination: Point) { + val routes = fetchRoute(destination) + navigationView.api.startActiveGuidance(routes) + } + + suspend fun startActiveGuidance(origin: Point, destination: Point) { + val routes = fetchRoute(origin, destination) + navigationView.api.startActiveGuidance(routes) + } + + suspend fun fetchRoute(destination: Point): List { + val origin = location.filterNotNull().first().toPoint() + return fetchRoute(origin, destination) + } + + suspend fun fetchRoute(origin: Point, destination: Point): List { + val mapboxNavigation = this.mapboxNavigation.filterNotNull().first() + return mapboxNavigation.fetchRoute(origin, destination) + } +} diff --git a/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/utils/TestRoutes.kt b/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/utils/TestRoutes.kt new file mode 100644 index 00000000000..edae3cd2f72 --- /dev/null +++ b/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/utils/TestRoutes.kt @@ -0,0 +1,69 @@ +package com.mapbox.navigation.examples.androidauto.utils + +import com.mapbox.geojson.Point + +/** + * Coordinates containing `subType = JCT` + * 139.7745686, 35.677573;139.784915, 35.680960 + * https://api.mapbox.com/guidance-views/v1/709948800/jct/CA075101?arrow_ids=CA07510E + * + * Coordinates containing `subType` = SAPA` + * 137.76136788022933, 34.83891088143494;137.75220947550804, 34.840924660770725 + * https://api.mapbox.com/guidance-views/v1/709948800/sapa/SA117201?arrow_ids=SA11720A + * + * Coordinates containing `subType` = CITYREAL` + * 139.68153626083233, 35.66812853462302;139.68850488593154, 35.66099697148769 + * https://api.mapbox.com/guidance-views/v1/709948800/cityreal/13c00282_o40d?arrow_ids=13c00282_o41a + * + * Coordinates containing `subType` = TOLLBRANCH` + * 137.02725, 35.468588;137.156787, 35.372602 + * https://api.mapbox.com/guidance-views/v1/709948800/tollbranch/CR896101?arrow_ids=CR89610A + * + * Coordinates containing `subType` = AFTERTOLL` + * 141.4223967090212, 43.07693368987961;141.42118630948409, 43.07604662044662 + * https://api.mapbox.com/guidance-views/v1/709948800/aftertoll/HW00101805?arrow_ids=HW00101805_1 + * + * Coordinates containing `subType` = EXPRESSWAY_ENTRANCE` + * 139.724088, 35.672885; 139.630359, 35.626416 + * https://api.mapbox.com/guidance-views/v1/709948800/entrance/13i00015_o10d?arrow_ids=13i00015_o11a + * + * Coordinates containing `subType` = EXPRESSWAY_EXIT` + * 135.324023, 34.715952;135.296332, 34.711387 + * https://api.mapbox.com/guidance-views/v1/709948800/exit/28o00022_o20d?arrow_ids=28o00022_o21a + */ +internal enum class TestRoutes( + val origin: Point, + val destination: Point +) { + JCT( + Point.fromLngLat(139.7745686, 35.677573), + Point.fromLngLat(139.784915, 35.680960) + ), + SAPA( + Point.fromLngLat(137.76136788022933, 34.83891088143494), + Point.fromLngLat(137.75220947550804, 34.840924660770725) + ), + CITYREAL( + Point.fromLngLat(139.68153626083233, 35.66812853462302), + Point.fromLngLat(139.68850488593154, 35.66099697148769) + ), + TOLLBRANCH( + Point.fromLngLat(137.02725, 35.468588), + Point.fromLngLat(137.156787, 35.372602) + ), + AFTERTOLL( + Point.fromLngLat(141.4223967090212, 43.07693368987961), + Point.fromLngLat(141.42118630948409, 43.07604662044662) + ), + EXPRESSWAY_ENTRANCE( + Point.fromLngLat(139.724088, 35.672885), + Point.fromLngLat(139.630359, 35.626416) + ), + EXPRESSWAY_EXIT( + Point.fromLngLat(135.324023, 34.715952), + Point.fromLngLat(135.296332, 34.711387) + ); + + operator fun component1(): Point = origin + operator fun component2(): Point = destination +} diff --git a/android-auto-app/src/main/res/drawable/ic_baseline_menu_24.xml b/android-auto-app/src/main/res/drawable/ic_baseline_menu_24.xml new file mode 100644 index 00000000000..dbeefd45f0f --- /dev/null +++ b/android-auto-app/src/main/res/drawable/ic_baseline_menu_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android-auto-app/src/main/res/drawable/menu_button_bg.xml b/android-auto-app/src/main/res/drawable/menu_button_bg.xml new file mode 100644 index 00000000000..e6e4f3f7b47 --- /dev/null +++ b/android-auto-app/src/main/res/drawable/menu_button_bg.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/android-auto-app/src/main/res/layout/activity_drawer.xml b/android-auto-app/src/main/res/layout/activity_drawer.xml new file mode 100644 index 00000000000..ce7f3254d2e --- /dev/null +++ b/android-auto-app/src/main/res/layout/activity_drawer.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + diff --git a/android-auto-app/src/main/res/layout/activity_main.xml b/android-auto-app/src/main/res/layout/activity_main.xml index 3f32eb819bb..27b1e43e4d5 100644 --- a/android-auto-app/src/main/res/layout/activity_main.xml +++ b/android-auto-app/src/main/res/layout/activity_main.xml @@ -1,20 +1,23 @@ - + android:layout_height="match_parent"> - + - - - + + \ No newline at end of file diff --git a/android-auto-app/src/main/res/layout/layout_drawer_menu_nav_view.xml b/android-auto-app/src/main/res/layout/layout_drawer_menu_nav_view.xml new file mode 100644 index 00000000000..4f761a749cc --- /dev/null +++ b/android-auto-app/src/main/res/layout/layout_drawer_menu_nav_view.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android-auto-app/src/main/res/layout/mapbox_activity_navigation_view.xml b/android-auto-app/src/main/res/layout/mapbox_activity_navigation_view.xml deleted file mode 100644 index ba8139fcc5a..00000000000 --- a/android-auto-app/src/main/res/layout/mapbox_activity_navigation_view.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/android-auto-app/src/main/res/values/strings.xml b/android-auto-app/src/main/res/values/strings.xml index c8b4118eb2f..0b6a38d0b2b 100644 --- a/android-auto-app/src/main/res/values/strings.xml +++ b/android-auto-app/src/main/res/values/strings.xml @@ -2,4 +2,14 @@ Dev Android Auto Hello blank fragment + + + JCT + SAPA + CITYREAL + TOLLBRANCH + AFTERTOLL + EXPRESSWAY_ENTRANCE + EXPRESSWAY_EXIT + \ No newline at end of file diff --git a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoMapper.kt b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoMapper.kt index 2f1a9207988..d80f32f38d1 100644 --- a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoMapper.kt +++ b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoMapper.kt @@ -2,9 +2,11 @@ package com.mapbox.androidauto.navigation import android.content.Context import android.text.SpannableStringBuilder +import androidx.car.app.model.CarIcon import androidx.car.app.navigation.model.NavigationTemplate import androidx.car.app.navigation.model.RoutingInfo import androidx.car.app.navigation.model.Step +import androidx.core.graphics.drawable.IconCompat import com.mapbox.androidauto.navigation.lanes.CarLanesImageRenderer import com.mapbox.androidauto.navigation.lanes.useMapboxLaneGuidance import com.mapbox.androidauto.navigation.maneuver.CarManeuverIconRenderer @@ -20,6 +22,7 @@ import com.mapbox.navigation.ui.maneuver.model.ManeuverPrimaryOptions import com.mapbox.navigation.ui.maneuver.model.ManeuverSecondaryOptions import com.mapbox.navigation.ui.maneuver.model.ManeuverSubOptions import com.mapbox.navigation.ui.maneuver.view.MapboxExitText +import com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionValue import com.mapbox.navigation.ui.shield.model.RouteShield /** @@ -42,6 +45,7 @@ class CarNavigationInfoMapper( expectedManeuvers: Expected>, routeShields: List, routeProgress: RouteProgress, + junctionValue: JunctionValue? ): NavigationTemplate.NavigationInfo? { val currentStepProgress = routeProgress.currentLegProgress?.currentStepProgress val distanceRemaining = currentStepProgress?.distanceRemaining ?: return null @@ -78,10 +82,22 @@ class CarNavigationInfoMapper( RoutingInfo.Builder() .setCurrentStep(step, stepDistance) .withOptionalNextStep(maneuver, routeShields) + .withOptionalJunctionImage(junctionValue) .build() } } + private fun RoutingInfo.Builder.withOptionalJunctionImage( + junctionValue: JunctionValue? + ) = apply { + junctionValue?.also { + val carIcon = CarIcon.Builder( + IconCompat.createWithBitmap(it.bitmap) + ).build() + setJunctionImage(carIcon) + } + } + private fun RoutingInfo.Builder.withOptionalNextStep( maneuver: Maneuver, routeShields: List diff --git a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoProvider.kt b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoProvider.kt index fac26df6747..2e5938a6f67 100644 --- a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoProvider.kt +++ b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoProvider.kt @@ -8,16 +8,20 @@ import androidx.car.app.navigation.model.TravelEstimate import androidx.lifecycle.lifecycleScope import com.mapbox.androidauto.internal.extensions.mapboxNavigationForward import com.mapbox.androidauto.internal.logAndroidAuto +import com.mapbox.api.directions.v5.models.BannerInstructions import com.mapbox.bindgen.Expected import com.mapbox.maps.extension.androidauto.MapboxCarMapObserver import com.mapbox.maps.extension.androidauto.MapboxCarMapSurface import com.mapbox.navigation.base.trip.model.RouteProgress import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp +import com.mapbox.navigation.core.trip.session.BannerInstructionsObserver import com.mapbox.navigation.core.trip.session.RouteProgressObserver import com.mapbox.navigation.ui.maneuver.api.MapboxManeuverApi import com.mapbox.navigation.ui.maneuver.model.Maneuver import com.mapbox.navigation.ui.maneuver.model.ManeuverError +import com.mapbox.navigation.ui.maps.guidance.junction.api.MapboxJunctionApi +import com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionValue import com.mapbox.navigation.ui.shield.model.RouteShield import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -44,9 +48,12 @@ internal constructor( private val mapUserStyleObserver = services.mapUserStyleObserver() private val routeProgressObserver = RouteProgressObserver(this::onRouteProgress) + private val bannerInstructionsObserver = + BannerInstructionsObserver(this::onNewBannerInstructions) private val navigationObserver = mapboxNavigationForward(this::onAttached, this::onDetached) private val _carNavigationInfo = MutableStateFlow(CarNavigationInfo()) private var currentShields = emptyList() + private var currentJunctionValue: JunctionValue? = null @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal var carContext: CarContext? = null @@ -60,6 +67,9 @@ internal constructor( @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal var maneuverApi: MapboxManeuverApi? = null + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal var junctionApi: MapboxJunctionApi? = null + /** * Contains data that helps populate the [NavigationTemplate] with navigation data. */ @@ -115,15 +125,21 @@ internal constructor( private fun onAttached(mapboxNavigation: MapboxNavigation) { val carContext = carContext!! maneuverApi = services.maneuverApi(mapboxNavigation) + junctionApi = services.junctionApi(mapboxNavigation) navigationEtaMapper = services.carNavigationEtaMapper(carContext) navigationInfoMapper = services.carNavigationInfoMapper(carContext, mapboxNavigation) mapboxNavigation.registerRouteProgressObserver(routeProgressObserver) + mapboxNavigation.registerBannerInstructionsObserver(bannerInstructionsObserver) } private fun onDetached(mapboxNavigation: MapboxNavigation) { maneuverApi!!.cancel() + junctionApi!!.cancelAll() mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver) + mapboxNavigation.unregisterBannerInstructionsObserver(bannerInstructionsObserver) maneuverApi = null + junctionApi = null + currentJunctionValue = null navigationEtaMapper = null navigationInfoMapper = null _carNavigationInfo.value = CarNavigationInfo() @@ -131,7 +147,7 @@ internal constructor( private fun onRouteProgress(routeProgress: RouteProgress) { val expectedManeuvers = maneuverApi?.getManeuvers(routeProgress) ?: return - updateNavigationInfo(expectedManeuvers, currentShields, routeProgress) + updateNavigationInfo(expectedManeuvers, routeProgress) expectedManeuvers.onValue { maneuvers -> maneuverApi?.getRoadShields( @@ -143,20 +159,28 @@ internal constructor( val newShields = shieldResult.mapNotNull { it.value?.shield } if (currentShields != newShields) { currentShields = newShields - updateNavigationInfo(expectedManeuvers, newShields, routeProgress) + updateNavigationInfo(expectedManeuvers, routeProgress) } } } } + private fun onNewBannerInstructions(bannerInstructions: BannerInstructions) { + junctionApi?.generateJunction(bannerInstructions) { + logAndroidAuto( + "CarNavigationInfoProvider junctionView: ${it.value ?: it.error?.errorMessage}" + ) + currentJunctionValue = it.value + } + } + private fun updateNavigationInfo( maneuvers: Expected>, - shields: List, routeProgress: RouteProgress, ) { _carNavigationInfo.value = CarNavigationInfo( navigationInfo = navigationInfoMapper - ?.mapNavigationInfo(maneuvers, shields, routeProgress), + ?.mapNavigationInfo(maneuvers, currentShields, routeProgress, currentJunctionValue), destinationTravelEstimate = navigationEtaMapper ?.getDestinationTravelEstimate(routeProgress) ) diff --git a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoServices.kt b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoServices.kt index 3efe6f560c0..a3ee336ad3a 100644 --- a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoServices.kt +++ b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoServices.kt @@ -1,13 +1,17 @@ package com.mapbox.androidauto.navigation +import android.content.Context import androidx.car.app.CarContext +import com.mapbox.androidauto.internal.logAndroidAutoFailure import com.mapbox.androidauto.navigation.lanes.CarLanesImageRenderer import com.mapbox.androidauto.navigation.maneuver.CarManeuverIconOptions import com.mapbox.androidauto.navigation.maneuver.CarManeuverIconRenderer import com.mapbox.androidauto.navigation.maneuver.CarManeuverInstructionRenderer +import com.mapbox.maps.MAPBOX_ACCESS_TOKEN_RESOURCE_NAME import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter import com.mapbox.navigation.ui.maneuver.api.MapboxManeuverApi +import com.mapbox.navigation.ui.maps.guidance.junction.api.MapboxJunctionApi import com.mapbox.navigation.ui.tripprogress.api.MapboxTripProgressApi import com.mapbox.navigation.ui.tripprogress.model.TripProgressUpdateFormatter @@ -41,6 +45,30 @@ internal class CarNavigationInfoServices { return MapboxManeuverApi(distanceFormatter) } + fun junctionApi(mapboxNavigation: MapboxNavigation): MapboxJunctionApi? { + val token = mapboxNavigation.getAccessToken() + if (token == null) { + logAndroidAutoFailure("Failed to create MapboxJunctionApi. Missing Mapbox ACCESS_TOKEN") + return null + } + return MapboxJunctionApi(token) + } + + private fun MapboxNavigation.getAccessToken(): String? { + val context = navigationOptions.applicationContext + return navigationOptions.accessToken ?: context.getResourceAccessToken() + } + + private fun Context.getResourceAccessToken(): String? = + runCatching { + val resId = resources.getIdentifier( + MAPBOX_ACCESS_TOKEN_RESOURCE_NAME, + "string", + packageName + ) + getString(resId) + }.getOrNull() + fun mapUserStyleObserver() = MapUserStyleObserver() private fun mapboxTripProgressApi(carContext: CarContext): MapboxTripProgressApi { diff --git a/libnavui-androidauto/src/test/java/com/mapbox/androidauto/navigation/CarNavigationInfoProviderTest.kt b/libnavui-androidauto/src/test/java/com/mapbox/androidauto/navigation/CarNavigationInfoProviderTest.kt index c9bfe9a2d84..540288fda1b 100644 --- a/libnavui-androidauto/src/test/java/com/mapbox/androidauto/navigation/CarNavigationInfoProviderTest.kt +++ b/libnavui-androidauto/src/test/java/com/mapbox/androidauto/navigation/CarNavigationInfoProviderTest.kt @@ -9,6 +9,7 @@ import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.trip.session.RouteProgressObserver import com.mapbox.navigation.testing.MainCoroutineRule import com.mapbox.navigation.ui.maneuver.api.MapboxManeuverApi +import com.mapbox.navigation.ui.maps.guidance.junction.api.MapboxJunctionApi import io.mockk.every import io.mockk.just import io.mockk.mockk @@ -37,11 +38,13 @@ class CarNavigationInfoProviderTest { private val carNavigationEtaMapper: CarNavigationEtaMapper = mockk(relaxed = true) private val carNavigationInfoMapper: CarNavigationInfoMapper = mockk(relaxed = true) private val maneuverApi: MapboxManeuverApi = mockk(relaxed = true) + private val junctionApi: MapboxJunctionApi = mockk(relaxed = true) private val serviceProvider: CarNavigationInfoServices = mockk { every { carNavigationEtaMapper(any()) } returns carNavigationEtaMapper every { carNavigationInfoMapper(any(), any()) } returns carNavigationInfoMapper every { maneuverApi(any()) } returns maneuverApi every { mapUserStyleObserver() } returns mockk(relaxed = true) + every { junctionApi(any()) } returns junctionApi } private val sut = CarNavigationInfoProvider(serviceProvider) @@ -61,6 +64,7 @@ class CarNavigationInfoProviderTest { val observerSlot = slot() val mapboxNavigation: MapboxNavigation = mockk { every { registerRouteProgressObserver(capture(observerSlot)) } just runs + every { registerBannerInstructionsObserver(any()) } just runs } val mapboxCarMapSurface: MapboxCarMapSurface = mockk(relaxed = true) @@ -92,6 +96,7 @@ class CarNavigationInfoProviderTest { val observerSlot = slot() val mapboxNavigation: MapboxNavigation = mockk { every { registerRouteProgressObserver(capture(observerSlot)) } just runs + every { registerBannerInstructionsObserver(any()) } just runs } val mapboxCarMapSurface: MapboxCarMapSurface = mockk(relaxed = true) From 8653b1e73d627cf285c908138c25ffa068894d55 Mon Sep 17 00:00:00 2001 From: Tomasz Rybakiewicz Date: Tue, 17 Jan 2023 17:00:57 -0500 Subject: [PATCH 2/8] Android Auto - CarLanesImageRenderer optimization. Cleanup. --- .../navigation/CarNavigationInfoProvider.kt | 3 -- .../navigation/lanes/CarLanesImageRenderer.kt | 28 +++++++++++-------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoProvider.kt b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoProvider.kt index 2e5938a6f67..cf87782c7aa 100644 --- a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoProvider.kt +++ b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoProvider.kt @@ -167,9 +167,6 @@ internal constructor( private fun onNewBannerInstructions(bannerInstructions: BannerInstructions) { junctionApi?.generateJunction(bannerInstructions) { - logAndroidAuto( - "CarNavigationInfoProvider junctionView: ${it.value ?: it.error?.errorMessage}" - ) currentJunctionValue = it.value } } diff --git a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/lanes/CarLanesImageRenderer.kt b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/lanes/CarLanesImageRenderer.kt index fa7b44cdb92..84565024943 100644 --- a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/lanes/CarLanesImageRenderer.kt +++ b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/lanes/CarLanesImageRenderer.kt @@ -2,11 +2,13 @@ package com.mapbox.androidauto.navigation.lanes import android.content.Context import android.graphics.Color +import android.util.LruCache import androidx.annotation.ColorInt import androidx.car.app.model.CarIcon import androidx.car.app.navigation.model.Step import com.mapbox.navigation.ui.maneuver.api.MapboxLaneIconsApi import com.mapbox.navigation.ui.maneuver.api.MapboxManeuverApi +import com.mapbox.navigation.ui.maneuver.model.Lane /** * This class generates a [CarLanesImage] needed for the lane guidance in android auto. @@ -20,6 +22,7 @@ class CarLanesImageRenderer( private val carLaneIconRenderer = CarLaneIconRenderer(context) private val laneIconsApi = MapboxLaneIconsApi() private val carLaneIconMapper = CarLaneMapper() + private val cache = LruCache(1) /** * Create the images needed to show lane guidance. @@ -27,19 +30,22 @@ class CarLanesImageRenderer( * @param lane retrieve the lane guidance through the [MapboxManeuverApi] * @return the lanes image, null when there is no lange guidance */ - fun renderLanesImage( - lane: com.mapbox.navigation.ui.maneuver.model.Lane? - ): CarLanesImage? { - return lane?.let { laneGuidance -> - val lanes = carLaneIconMapper.mapLanes(laneGuidance) - val carIcon = carIcon(laneGuidance) - CarLanesImage(lanes, carIcon) + fun renderLanesImage(lane: Lane?): CarLanesImage? { + return lane?.let { + cache.get(lane)?.also { + return it + } + + val img = CarLanesImage( + lanes = carLaneIconMapper.mapLanes(lane), + carIcon = lanesCarIcon(lane) + ) + cache.put(lane, img) + img } } - private fun carIcon( - laneGuidance: com.mapbox.navigation.ui.maneuver.model.Lane - ): CarIcon { + private fun lanesCarIcon(laneGuidance: Lane): CarIcon { val carLaneIcons = laneGuidance.allLanes.map { laneIndicator -> val laneIcon = laneIconsApi.getTurnLane(laneIndicator) CarLaneIcon( @@ -60,7 +66,7 @@ class CarLanesImageRenderer( */ fun Step.Builder.useMapboxLaneGuidance( imageGenerator: CarLanesImageRenderer, - laneGuidance: com.mapbox.navigation.ui.maneuver.model.Lane? + laneGuidance: Lane? ) = apply { val lanesImage = imageGenerator.renderLanesImage(laneGuidance) if (lanesImage != null) { From 60e73ae6dbd6f8507dc3dd06d2a1d50430e6d0ce Mon Sep 17 00:00:00 2001 From: Tomasz Rybakiewicz Date: Wed, 18 Jan 2023 14:24:49 -0500 Subject: [PATCH 3/8] Android Auto: Unit tests --- .../lanes/CarLanesImageRendererTest.kt | 21 ++ .../navigation/CarNavigationInfoMapperTest.kt | 275 ++++++++++++++++++ 2 files changed, 296 insertions(+) create mode 100644 libnavui-androidauto/src/test/java/com/mapbox/androidauto/navigation/CarNavigationInfoMapperTest.kt diff --git a/libnavui-androidauto/src/androidTest/java/com/mapbox/androidauto/car/navigation/lanes/CarLanesImageRendererTest.kt b/libnavui-androidauto/src/androidTest/java/com/mapbox/androidauto/car/navigation/lanes/CarLanesImageRendererTest.kt index 77927d2b970..c6ceb7c3d18 100644 --- a/libnavui-androidauto/src/androidTest/java/com/mapbox/androidauto/car/navigation/lanes/CarLanesImageRendererTest.kt +++ b/libnavui-androidauto/src/androidTest/java/com/mapbox/androidauto/car/navigation/lanes/CarLanesImageRendererTest.kt @@ -7,9 +7,11 @@ import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner import androidx.test.rule.GrantPermissionRule import com.mapbox.androidauto.navigation.lanes.CarLanesImageRenderer import com.mapbox.androidauto.testing.BitmapTestUtil +import com.mapbox.navigation.ui.maneuver.model.Lane import io.mockk.every import io.mockk.mockk import org.junit.Assert.assertNotNull +import org.junit.Assert.assertSame import org.junit.Rule import org.junit.Test import org.junit.rules.TestName @@ -39,6 +41,25 @@ class CarLanesImageRendererTest { background = Color.RED ) + @Test + fun cache_test() { + val lane = mockk { + every { allLanes } returns listOf( + mockk { + every { drivingSide } returns "right" + every { activeDirection } returns "uturn" + every { isActive } returns true + every { directions } returns listOf("uturn") + } + ) + } + val img1 = carLanesImageGenerator.renderLanesImage(lane) + val img2 = carLanesImageGenerator.renderLanesImage(lane) + + assertNotNull(img1) + assertSame(img1, img2) + } + @Test fun one_lane_uturn() { val carLanesImage = carLanesImageGenerator.renderLanesImage( diff --git a/libnavui-androidauto/src/test/java/com/mapbox/androidauto/navigation/CarNavigationInfoMapperTest.kt b/libnavui-androidauto/src/test/java/com/mapbox/androidauto/navigation/CarNavigationInfoMapperTest.kt new file mode 100644 index 00000000000..53f327de81b --- /dev/null +++ b/libnavui-androidauto/src/test/java/com/mapbox/androidauto/navigation/CarNavigationInfoMapperTest.kt @@ -0,0 +1,275 @@ +package com.mapbox.androidauto.navigation + +import android.content.Context +import android.graphics.Bitmap +import android.os.Build +import androidx.car.app.model.CarIcon +import androidx.car.app.model.CarText +import androidx.car.app.model.Distance +import androidx.car.app.navigation.model.Lane +import androidx.car.app.navigation.model.LaneDirection +import androidx.car.app.navigation.model.RoutingInfo +import androidx.core.graphics.drawable.IconCompat +import androidx.test.core.app.ApplicationProvider +import com.mapbox.androidauto.navigation.lanes.CarLanesImage +import com.mapbox.androidauto.navigation.lanes.CarLanesImageRenderer +import com.mapbox.androidauto.navigation.maneuver.CarManeuverIconRenderer +import com.mapbox.androidauto.navigation.maneuver.CarManeuverInstructionRenderer +import com.mapbox.api.directions.v5.models.ManeuverModifier +import com.mapbox.api.directions.v5.models.StepManeuver +import com.mapbox.bindgen.ExpectedFactory +import com.mapbox.geojson.Point +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.ui.maneuver.model.Component +import com.mapbox.navigation.ui.maneuver.model.LaneFactory +import com.mapbox.navigation.ui.maneuver.model.LaneIndicator +import com.mapbox.navigation.ui.maneuver.model.Maneuver +import com.mapbox.navigation.ui.maneuver.model.ManeuverFactory +import com.mapbox.navigation.ui.maneuver.model.PrimaryManeuverFactory +import com.mapbox.navigation.ui.maneuver.model.RoadShieldComponentNode +import com.mapbox.navigation.ui.maneuver.model.SecondaryManeuverFactory +import com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionValue +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@OptIn(ExperimentalMapboxNavigationAPI::class) +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.O]) +class CarNavigationInfoMapperTest { + + private lateinit var instructionRenderer: CarManeuverInstructionRenderer + private lateinit var iconRenderer: CarManeuverIconRenderer + private lateinit var imageGenerator: CarLanesImageRenderer + private lateinit var sut: CarNavigationInfoMapper + + @Before + fun setup() { + mockkStatic(CarDistanceFormatter::class) + every { CarDistanceFormatter.carDistance(any()) } answers { + Distance.create(firstArg(), Distance.UNIT_METERS) + } + val context = ApplicationProvider.getApplicationContext() + instructionRenderer = mockk(relaxed = true) + iconRenderer = mockk(relaxed = true) + imageGenerator = mockk(relaxed = true) + + sut = CarNavigationInfoMapper( + context, + instructionRenderer, + iconRenderer, + imageGenerator + ) + } + + @After + fun teardown() { + unmockkAll() + } + + @Test + fun `mapNavigationInfo - should return NULL when distanceRemaining data is not available`() { + val routeProgress = mockk { + every { currentLegProgress } returns mockk { + every { currentStepProgress } returns null + } + } + + val result = sut.mapNavigationInfo( + expectedManeuvers = ExpectedFactory.createError(mockk()), + routeShields = emptyList(), + routeProgress = routeProgress, + junctionValue = null + ) + + assertNull(result) + } + + @Test + @Suppress("MaxLineLength") + fun `mapNavigationInfo - should return RoutingInfo with Maneuver info`() { + val renderedPrimaryInstruction = "rendered primary maneuver instruction" + val renderedSecondaryInstruction = "rendered secondary maneuver instruction" + given( + renderedPrimaryInstruction = renderedPrimaryInstruction, + renderedSecondaryInstruction = renderedSecondaryInstruction + ) + + val result = sut.mapNavigationInfo( + expectedManeuvers = ExpectedFactory.createValue(listOf(TEST_MANEUVER)), + routeShields = emptyList(), + routeProgress = TEST_ROUTE_PROGRESS, + junctionValue = null + ) as RoutingInfo + + val step = result.currentStep + assertNotNull(step) + assertEquals( + CarText.create( + "$renderedPrimaryInstruction\n$renderedSecondaryInstruction" + ).toCharSequence(), + step!!.cue!!.toCharSequence() + ) + assertEquals( + androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT, + step.maneuver!!.type + ) + } + + @Test + @Suppress("MaxLineLength") + fun `mapNavigationInfo - should return RoutingInfo with lane guidance info`() { + val renderedLanesImage = CarLanesImage( + listOf( + Lane.Builder() + .addDirection(LaneDirection.create(LaneDirection.SHAPE_STRAIGHT, false)) + .addDirection(LaneDirection.create(LaneDirection.SHAPE_NORMAL_RIGHT, true)) + .build() + ), + CarIcon.Builder(IconCompat.createWithBitmap(sampleBitmap())).build() + ) + given( + renderedPrimaryInstruction = "rendered primary maneuver instruction", + renderedSecondaryInstruction = "rendered secondary maneuver instruction", + renderedLanesImage = renderedLanesImage + ) + + val result = sut.mapNavigationInfo( + expectedManeuvers = ExpectedFactory.createValue(listOf(TEST_MANEUVER)), + routeShields = emptyList(), + routeProgress = TEST_ROUTE_PROGRESS, + junctionValue = null + ) as RoutingInfo + + assertEquals(renderedLanesImage.carIcon, result.currentStep!!.lanesImage) + } + + @Test + @Suppress("MaxLineLength") + fun `mapNavigationInfo - should return RoutingInfo with an optional junction image`() { + val junctionBitmap = sampleBitmap() + val junctionValue = mockk { + every { bitmap } returns junctionBitmap + } + given( + renderedPrimaryInstruction = "rendered primary maneuver instruction", + renderedSecondaryInstruction = "rendered secondary maneuver instruction", + ) + + val result = sut.mapNavigationInfo( + expectedManeuvers = ExpectedFactory.createValue(listOf(TEST_MANEUVER)), + routeShields = emptyList(), + routeProgress = TEST_ROUTE_PROGRESS, + junctionValue = junctionValue + ) as RoutingInfo + + assertEquals( + CarIcon.Builder(IconCompat.createWithBitmap(junctionBitmap)).build(), + result.junctionImage + ) + } + + private fun given( + renderedPrimaryInstruction: String, + renderedSecondaryInstruction: String, + renderedLanesImage: CarLanesImage? = null + ) { + every { + instructionRenderer.renderInstruction( + maneuver = TEST_MANEUVER.primary.componentList, + shields = any(), + exitView = any(), + modifier = TEST_MANEUVER.primary.modifier, + any() + ) + } returns renderedPrimaryInstruction + every { + instructionRenderer.renderInstruction( + maneuver = TEST_MANEUVER.secondary!!.componentList, + shields = any(), + exitView = any(), + modifier = TEST_MANEUVER.secondary!!.modifier, + any() + ) + } returns renderedSecondaryInstruction + every { + imageGenerator.renderLanesImage(any()) + } returns renderedLanesImage + } + + @Suppress("PrivatePropertyName") + private val TEST_ROUTE_PROGRESS = mockk { + every { currentLegProgress } returns mockk { + every { currentStepProgress } returns mockk { + every { distanceRemaining } returns 1000f + } + } + } + + @Suppress("PrivatePropertyName") + private val MANEUVER_COMPONENT_ROAD_SHIELD1: Component = Component( + type = "", + node = RoadShieldComponentNode.Builder() + .shieldUrl("https://shield.mapbox.com/primary/url1") + .text("") + .mapboxShield(null) + .build() + ) + + @Suppress("PrivatePropertyName") + private val MANEUVER_COMPONENT_ROAD_SHIELD2: Component = Component( + type = "", + node = RoadShieldComponentNode.Builder() + .shieldUrl("https://shield.mapbox.com/primary/url2") + .text("") + .mapboxShield(null) + .build() + ) + + @Suppress("PrivatePropertyName") + private val TEST_MANEUVER: Maneuver = ManeuverFactory.buildManeuver( + primary = PrimaryManeuverFactory.buildPrimaryManeuver( + id = "primary_0", + text = "Turn Right", + type = StepManeuver.TURN, + degrees = 0.0, + modifier = ManeuverModifier.RIGHT, + drivingSide = "right", + componentList = listOf( + MANEUVER_COMPONENT_ROAD_SHIELD1, + MANEUVER_COMPONENT_ROAD_SHIELD2, + ) + ), + stepDistance = mockk(), + secondary = SecondaryManeuverFactory.buildSecondaryManeuver( + id = "secondary_0", + text = "Continue Straight", + type = StepManeuver.CONTINUE, + degrees = 0.0, + modifier = ManeuverModifier.STRAIGHT, + drivingSide = "right", + componentList = emptyList(), + ), + sub = null, + lane = LaneFactory.buildLane( + listOf( + LaneIndicator.Builder().directions(listOf("straight")).isActive(false).build(), + LaneIndicator.Builder().directions(listOf("right")).isActive(true).build() + ) + ), + point = Point.fromLngLat(10.0, 20.0) + ) + + private fun sampleBitmap() = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) +} From 0e2389b48303d0585293f01f2cffe8fe86539981 Mon Sep 17 00:00:00 2001 From: Tomasz Rybakiewicz Date: Wed, 18 Jan 2023 21:26:57 -0500 Subject: [PATCH 4/8] Changelog entry. Metalava file regen. Cleanup. --- .../mapbox/navigation/examples/androidauto/app/MainActivity.kt | 1 - libnavui-androidauto/api/current.txt | 2 +- .../changelog/unreleased/features/JunctionViewSupport.md | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 libnavui-androidauto/changelog/unreleased/features/JunctionViewSupport.md diff --git a/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/MainActivity.kt b/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/MainActivity.kt index ddfa9ae9583..faebdedd96c 100644 --- a/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/MainActivity.kt +++ b/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/MainActivity.kt @@ -12,7 +12,6 @@ import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.internal.extensions.flowRoutesUpdated import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp import com.mapbox.navigation.core.trip.session.BannerInstructionsObserver -import com.mapbox.navigation.dropin.navigationview.NavigationViewListener import com.mapbox.navigation.examples.androidauto.CarAppSyncComponent import com.mapbox.navigation.examples.androidauto.databinding.ActivityMainBinding import com.mapbox.navigation.examples.androidauto.databinding.LayoutDrawerMenuNavViewBinding diff --git a/libnavui-androidauto/api/current.txt b/libnavui-androidauto/api/current.txt index 5ef957a5e6a..1d094663b15 100644 --- a/libnavui-androidauto/api/current.txt +++ b/libnavui-androidauto/api/current.txt @@ -341,7 +341,7 @@ package com.mapbox.androidauto.navigation { public final class CarNavigationInfoMapper { ctor public CarNavigationInfoMapper(android.content.Context context, com.mapbox.androidauto.navigation.maneuver.CarManeuverInstructionRenderer carManeuverInstructionRenderer, com.mapbox.androidauto.navigation.maneuver.CarManeuverIconRenderer carManeuverIconRenderer, com.mapbox.androidauto.navigation.lanes.CarLanesImageRenderer carLanesImageGenerator); - method public androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo? mapNavigationInfo(com.mapbox.bindgen.Expected> expectedManeuvers, java.util.List routeShields, com.mapbox.navigation.base.trip.model.RouteProgress routeProgress); + method public androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo? mapNavigationInfo(com.mapbox.bindgen.Expected> expectedManeuvers, java.util.List routeShields, com.mapbox.navigation.base.trip.model.RouteProgress routeProgress, com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionValue? junctionValue); } public final class CarNavigationInfoProvider implements com.mapbox.maps.extension.androidauto.MapboxCarMapObserver { diff --git a/libnavui-androidauto/changelog/unreleased/features/JunctionViewSupport.md b/libnavui-androidauto/changelog/unreleased/features/JunctionViewSupport.md new file mode 100644 index 00000000000..9ea59ba416e --- /dev/null +++ b/libnavui-androidauto/changelog/unreleased/features/JunctionViewSupport.md @@ -0,0 +1 @@ +- Added support for Junction Views. \ No newline at end of file From 4af33ddb7285874cf0b88b8a92ba96cff318ff02 Mon Sep 17 00:00:00 2001 From: runner Date: Thu, 19 Jan 2023 02:27:33 +0000 Subject: [PATCH 5/8] Rename changelog files --- .../unreleased/features/{JunctionViewSupport.md => 6849.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename libnavui-androidauto/changelog/unreleased/features/{JunctionViewSupport.md => 6849.md} (100%) diff --git a/libnavui-androidauto/changelog/unreleased/features/JunctionViewSupport.md b/libnavui-androidauto/changelog/unreleased/features/6849.md similarity index 100% rename from libnavui-androidauto/changelog/unreleased/features/JunctionViewSupport.md rename to libnavui-androidauto/changelog/unreleased/features/6849.md From 463d1d1632da94f4ab9bf772c815d135fd98091e Mon Sep 17 00:00:00 2001 From: Tomasz Rybakiewicz Date: Thu, 19 Jan 2023 14:21:26 -0500 Subject: [PATCH 6/8] Addressing PR feedback. Updated CarNavigationInfoMapper to avoid SEMVER breaking. --- .../examples/androidauto/app/MainActivity.kt | 4 +-- libnavui-androidauto/api/current.txt | 3 +- .../navigation/CarNavigationInfoMapper.kt | 3 +- .../CarNavigationInfoProviderTest.kt | 35 +++++++++++++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/MainActivity.kt b/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/MainActivity.kt index faebdedd96c..1e35b046f24 100644 --- a/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/MainActivity.kt +++ b/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/MainActivity.kt @@ -59,10 +59,10 @@ class MainActivity : DrawerActivity() { menuBinding.junctionViewTestButton.setOnClickListener { lifecycleScope.launch { - val (or, de) = TestRoutes.valueOf( + val (origin, destination) = TestRoutes.valueOf( menuBinding.spinnerTestRoute.selectedItem as String ) - controller.startActiveGuidance(or, de) + controller.startActiveGuidance(origin, destination) closeDrawers() } } diff --git a/libnavui-androidauto/api/current.txt b/libnavui-androidauto/api/current.txt index 1d094663b15..41dda537bbc 100644 --- a/libnavui-androidauto/api/current.txt +++ b/libnavui-androidauto/api/current.txt @@ -341,7 +341,8 @@ package com.mapbox.androidauto.navigation { public final class CarNavigationInfoMapper { ctor public CarNavigationInfoMapper(android.content.Context context, com.mapbox.androidauto.navigation.maneuver.CarManeuverInstructionRenderer carManeuverInstructionRenderer, com.mapbox.androidauto.navigation.maneuver.CarManeuverIconRenderer carManeuverIconRenderer, com.mapbox.androidauto.navigation.lanes.CarLanesImageRenderer carLanesImageGenerator); - method public androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo? mapNavigationInfo(com.mapbox.bindgen.Expected> expectedManeuvers, java.util.List routeShields, com.mapbox.navigation.base.trip.model.RouteProgress routeProgress, com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionValue? junctionValue); + method public androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo? mapNavigationInfo(com.mapbox.bindgen.Expected> expectedManeuvers, java.util.List routeShields, com.mapbox.navigation.base.trip.model.RouteProgress routeProgress, com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionValue? junctionValue = null); + method public androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo? mapNavigationInfo(com.mapbox.bindgen.Expected> expectedManeuvers, java.util.List routeShields, com.mapbox.navigation.base.trip.model.RouteProgress routeProgress); } public final class CarNavigationInfoProvider implements com.mapbox.maps.extension.androidauto.MapboxCarMapObserver { diff --git a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoMapper.kt b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoMapper.kt index d80f32f38d1..58071b71271 100644 --- a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoMapper.kt +++ b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/CarNavigationInfoMapper.kt @@ -41,11 +41,12 @@ class CarNavigationInfoMapper( private val secondaryExitOptions = ManeuverSecondaryOptions.Builder().build().exitOptions private val subExitOptions = ManeuverSubOptions.Builder().build().exitOptions + @JvmOverloads fun mapNavigationInfo( expectedManeuvers: Expected>, routeShields: List, routeProgress: RouteProgress, - junctionValue: JunctionValue? + junctionValue: JunctionValue? = null ): NavigationTemplate.NavigationInfo? { val currentStepProgress = routeProgress.currentLegProgress?.currentStepProgress val distanceRemaining = currentStepProgress?.distanceRemaining ?: return null diff --git a/libnavui-androidauto/src/test/java/com/mapbox/androidauto/navigation/CarNavigationInfoProviderTest.kt b/libnavui-androidauto/src/test/java/com/mapbox/androidauto/navigation/CarNavigationInfoProviderTest.kt index 540288fda1b..36c5a05ab15 100644 --- a/libnavui-androidauto/src/test/java/com/mapbox/androidauto/navigation/CarNavigationInfoProviderTest.kt +++ b/libnavui-androidauto/src/test/java/com/mapbox/androidauto/navigation/CarNavigationInfoProviderTest.kt @@ -4,12 +4,19 @@ import androidx.car.app.Screen import androidx.car.app.navigation.model.NavigationTemplate import androidx.lifecycle.testing.TestLifecycleOwner import com.mapbox.androidauto.testing.CarAppTestRule +import com.mapbox.bindgen.Expected +import com.mapbox.bindgen.ExpectedFactory import com.mapbox.maps.extension.androidauto.MapboxCarMapSurface +import com.mapbox.navigation.base.trip.model.RouteProgress import com.mapbox.navigation.core.MapboxNavigation +import com.mapbox.navigation.core.trip.session.BannerInstructionsObserver import com.mapbox.navigation.core.trip.session.RouteProgressObserver import com.mapbox.navigation.testing.MainCoroutineRule +import com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer import com.mapbox.navigation.ui.maneuver.api.MapboxManeuverApi import com.mapbox.navigation.ui.maps.guidance.junction.api.MapboxJunctionApi +import com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionError +import com.mapbox.navigation.ui.maps.guidance.junction.model.JunctionValue import io.mockk.every import io.mockk.just import io.mockk.mockk @@ -17,6 +24,7 @@ import io.mockk.runs import io.mockk.slot import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Rule @@ -91,6 +99,33 @@ class CarNavigationInfoProviderTest { assertNull(sut.carNavigationInfo.value.navigationInfo) } + @Test + fun `junctionView is available before route progress`() = runBlockingTest { + val routeProgress = mockk(relaxed = true) + val junctionValue = mockk(relaxed = true) + val progressObserver = slot() + val instrObserver = slot() + val mapboxNavigation: MapboxNavigation = mockk { + every { registerRouteProgressObserver(capture(progressObserver)) } just runs + every { registerBannerInstructionsObserver(capture(instrObserver)) } just runs + } + every { junctionApi.generateJunction(any(), any()) } answers { + secondArg>>() + .accept(ExpectedFactory.createValue(junctionValue)) + } + val mapboxCarMapSurface: MapboxCarMapSurface = mockk(relaxed = true) + + carAppTestRule.onAttached(mapboxNavigation) + sut.onAttached(mapboxCarMapSurface) + instrObserver.captured.onNewBannerInstructions(mockk(relaxed = true)) + progressObserver.captured.onRouteProgressChanged(routeProgress) + + verify { + carNavigationInfoMapper.mapNavigationInfo(any(), any(), routeProgress, junctionValue) + } + assertNotNull(sut.carNavigationInfo.value.navigationInfo) + } + @Test fun `travelEstimate is available after route progress`() { val observerSlot = slot() From b81f614dc79962d2fbce0eeff886b8a53e0d7fe6 Mon Sep 17 00:00:00 2001 From: Tomasz Rybakiewicz Date: Thu, 19 Jan 2023 14:28:17 -0500 Subject: [PATCH 7/8] build.gradle file cleanup --- android-auto-app/build.gradle | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/android-auto-app/build.gradle b/android-auto-app/build.gradle index de9f8eb0a77..7c53a6f6a66 100644 --- a/android-auto-app/build.gradle +++ b/android-auto-app/build.gradle @@ -75,7 +75,7 @@ dependencies { implementation("com.mapbox.navigation:ui-dropin:2.10.0-rc.1") implementation("com.mapbox.search:mapbox-search-android:1.0.0-beta.42") - // Support libraries + // Dependencies needed for this example. implementation dependenciesList.androidXCore implementation dependenciesList.materialDesign implementation dependenciesList.androidXAppCompat @@ -84,7 +84,4 @@ dependencies { implementation dependenciesList.androidXFragment implementation dependenciesList.androidXLifecycleLivedata implementation dependenciesList.androidXLifecycleRuntime - - // Dependencies needed for this example. - implementation dependenciesList.androidXAppCompat } \ No newline at end of file From ae83881f7d6c1c50faaed154d3ef371df84ffc14 Mon Sep 17 00:00:00 2001 From: Tomasz Rybakiewicz Date: Fri, 20 Jan 2023 10:27:10 -0500 Subject: [PATCH 8/8] Added both cache_hit and cache_miss tests --- .../lanes/CarLanesImageRendererTest.kt | 69 +++++++++++++++---- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/libnavui-androidauto/src/androidTest/java/com/mapbox/androidauto/car/navigation/lanes/CarLanesImageRendererTest.kt b/libnavui-androidauto/src/androidTest/java/com/mapbox/androidauto/car/navigation/lanes/CarLanesImageRendererTest.kt index c6ceb7c3d18..7577c3863da 100644 --- a/libnavui-androidauto/src/androidTest/java/com/mapbox/androidauto/car/navigation/lanes/CarLanesImageRendererTest.kt +++ b/libnavui-androidauto/src/androidTest/java/com/mapbox/androidauto/car/navigation/lanes/CarLanesImageRendererTest.kt @@ -7,16 +7,20 @@ import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner import androidx.test.rule.GrantPermissionRule import com.mapbox.androidauto.navigation.lanes.CarLanesImageRenderer import com.mapbox.androidauto.testing.BitmapTestUtil -import com.mapbox.navigation.ui.maneuver.model.Lane +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI +import com.mapbox.navigation.ui.maneuver.model.LaneFactory +import com.mapbox.navigation.ui.maneuver.model.LaneIndicator import io.mockk.every import io.mockk.mockk import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNotSame import org.junit.Assert.assertSame import org.junit.Rule import org.junit.Test import org.junit.rules.TestName import org.junit.runner.RunWith +@OptIn(ExperimentalMapboxNavigationAPI::class) @RunWith(AndroidJUnit4ClassRunner::class) @SmallTest class CarLanesImageRendererTest { @@ -42,24 +46,63 @@ class CarLanesImageRendererTest { ) @Test - fun cache_test() { - val lane = mockk { - every { allLanes } returns listOf( - mockk { - every { drivingSide } returns "right" - every { activeDirection } returns "uturn" - every { isActive } returns true - every { directions } returns listOf("uturn") - } + fun cache_hit() { + val lane1 = LaneFactory.buildLane( + allLanes = listOf( + LaneIndicator.Builder() + .drivingSide("right") + .activeDirection("uturn") + .isActive(true) + .directions(listOf("uturn")) + .build() ) - } - val img1 = carLanesImageGenerator.renderLanesImage(lane) - val img2 = carLanesImageGenerator.renderLanesImage(lane) + ) + val lane2 = LaneFactory.buildLane( + allLanes = listOf( + LaneIndicator.Builder() + .drivingSide("right") + .activeDirection("uturn") + .isActive(true) + .directions(listOf("uturn")) + .build() + ) + ) + val img1 = carLanesImageGenerator.renderLanesImage(lane1) + val img2 = carLanesImageGenerator.renderLanesImage(lane2) assertNotNull(img1) assertSame(img1, img2) } + @Test + fun cache_miss() { + val lane1 = LaneFactory.buildLane( + allLanes = listOf( + LaneIndicator.Builder() + .drivingSide("right") + .activeDirection("uturn") + .isActive(true) + .directions(listOf("uturn")) + .build() + ) + ) + val lane2 = LaneFactory.buildLane( + allLanes = listOf( + LaneIndicator.Builder() + .drivingSide("right") + .activeDirection("straight") + .isActive(true) + .directions(listOf("straight")) + .build() + ) + ) + val img1 = carLanesImageGenerator.renderLanesImage(lane1) + val img2 = carLanesImageGenerator.renderLanesImage(lane2) + + assertNotNull(img1) + assertNotSame(img1, img2) + } + @Test fun one_lane_uturn() { val carLanesImage = carLanesImageGenerator.renderLanesImage(