diff --git a/android-auto-app/build.gradle b/android-auto-app/build.gradle index 750b69f81e9..7c53a6f6a66 100644 --- a/android-auto-app/build.gradle +++ b/android-auto-app/build.gradle @@ -76,5 +76,12 @@ dependencies { implementation("com.mapbox.search:mapbox-search-android:1.0.0-beta.42") // Dependencies needed for this example. + implementation dependenciesList.androidXCore + implementation dependenciesList.materialDesign implementation dependenciesList.androidXAppCompat + implementation dependenciesList.androidXCardView + implementation dependenciesList.androidXConstraintLayout + implementation dependenciesList.androidXFragment + implementation dependenciesList.androidXLifecycleLivedata + implementation dependenciesList.androidXLifecycleRuntime } \ 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..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 @@ -1,23 +1,117 @@ - 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.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 (origin, destination) = TestRoutes.valueOf( + menuBinding.spinnerTestRoute.selectedItem as String + ) + controller.startActiveGuidance(origin, destination) + 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/api/current.txt b/libnavui-androidauto/api/current.txt index 5ef957a5e6a..41dda537bbc 100644 --- a/libnavui-androidauto/api/current.txt +++ b/libnavui-androidauto/api/current.txt @@ -341,6 +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, 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); } diff --git a/libnavui-androidauto/changelog/unreleased/features/6849.md b/libnavui-androidauto/changelog/unreleased/features/6849.md new file mode 100644 index 00000000000..9ea59ba416e --- /dev/null +++ b/libnavui-androidauto/changelog/unreleased/features/6849.md @@ -0,0 +1 @@ +- Added support for Junction Views. \ No newline at end of file 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..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,14 +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.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 { @@ -39,6 +45,64 @@ class CarLanesImageRendererTest { background = Color.RED ) + @Test + fun cache_hit() { + 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("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( 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..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 @@ -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 /** @@ -38,10 +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? = null ): NavigationTemplate.NavigationInfo? { val currentStepProgress = routeProgress.currentLegProgress?.currentStepProgress val distanceRemaining = currentStepProgress?.distanceRemaining ?: return null @@ -78,10 +83,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..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 @@ -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,25 @@ 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) { + 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/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) { 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) +} 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..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,11 +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 @@ -16,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 @@ -37,11 +46,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 +72,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) @@ -87,11 +99,39 @@ 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() val mapboxNavigation: MapboxNavigation = mockk { every { registerRouteProgressObserver(capture(observerSlot)) } just runs + every { registerBannerInstructionsObserver(any()) } just runs } val mapboxCarMapSurface: MapboxCarMapSurface = mockk(relaxed = true)