Skip to content

Commit 5f67450

Browse files
[Android Auto] Add support for Junction Views (#6849)
* Android Auto - added support for Junction Views - CarLanesImageRenderer optimization. Cleanup.
1 parent e0c5710 commit 5f67450

File tree

23 files changed

+1057
-50
lines changed

23 files changed

+1057
-50
lines changed

android-auto-app/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,12 @@ dependencies {
7676
implementation("com.mapbox.search:mapbox-search-android:1.0.0-beta.42")
7777

7878
// Dependencies needed for this example.
79+
implementation dependenciesList.androidXCore
80+
implementation dependenciesList.materialDesign
7981
implementation dependenciesList.androidXAppCompat
82+
implementation dependenciesList.androidXCardView
83+
implementation dependenciesList.androidXConstraintLayout
84+
implementation dependenciesList.androidXFragment
85+
implementation dependenciesList.androidXLifecycleLivedata
86+
implementation dependenciesList.androidXLifecycleRuntime
8087
}

android-auto-app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
android:theme="@style/Theme.MapboxNavigationExamples">
1313
<activity
1414
android:name=".app.MainActivity"
15+
android:theme="@style/Theme.AppCompat.NoActionBar"
1516
android:exported="true">
1617
<intent-filter>
1718
<action android:name="android.intent.action.MAIN" />
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.mapbox.navigation.examples.androidauto.app
2+
3+
import android.os.Bundle
4+
import android.view.View
5+
import android.widget.AdapterView
6+
import android.widget.SpinnerAdapter
7+
import androidx.annotation.CallSuper
8+
import androidx.appcompat.app.AppCompatActivity
9+
import androidx.appcompat.widget.AppCompatSpinner
10+
import androidx.appcompat.widget.SwitchCompat
11+
import androidx.core.view.GravityCompat
12+
import androidx.lifecycle.MutableLiveData
13+
import com.mapbox.navigation.examples.androidauto.databinding.ActivityDrawerBinding
14+
15+
abstract class DrawerActivity : AppCompatActivity() {
16+
17+
private lateinit var binding: ActivityDrawerBinding
18+
19+
@CallSuper
20+
override fun onCreate(savedInstanceState: Bundle?) {
21+
super.onCreate(savedInstanceState)
22+
binding = ActivityDrawerBinding.inflate(layoutInflater)
23+
binding.drawerContent.addView(onCreateContentView(), 0)
24+
binding.drawerMenuContent.addView(onCreateMenuView())
25+
setContentView(binding.root)
26+
27+
binding.menuButton.setOnClickListener { openDrawer() }
28+
}
29+
30+
abstract fun onCreateContentView(): View
31+
32+
abstract fun onCreateMenuView(): View
33+
34+
fun openDrawer() {
35+
binding.drawerLayout.openDrawer(GravityCompat.START)
36+
}
37+
38+
fun closeDrawers() {
39+
binding.drawerLayout.closeDrawers()
40+
}
41+
42+
protected fun bindSwitch(
43+
switch: SwitchCompat,
44+
getValue: () -> Boolean,
45+
setValue: (v: Boolean) -> Unit
46+
) {
47+
switch.isChecked = getValue()
48+
switch.setOnCheckedChangeListener { _, isChecked -> setValue(isChecked) }
49+
}
50+
51+
protected fun bindSwitch(
52+
switch: SwitchCompat,
53+
liveData: MutableLiveData<Boolean>,
54+
onChange: (value: Boolean) -> Unit
55+
) {
56+
liveData.observe(this) {
57+
switch.isChecked = it
58+
onChange(it)
59+
}
60+
switch.setOnCheckedChangeListener { _, isChecked ->
61+
liveData.value = isChecked
62+
}
63+
}
64+
65+
protected fun bindSpinner(
66+
spinner: AppCompatSpinner,
67+
liveData: MutableLiveData<String>,
68+
onChange: (value: String) -> Unit
69+
) {
70+
liveData.observe(this) {
71+
if (spinner.selectedItem != it) {
72+
spinner.setSelection(spinner.adapter.findItemPosition(it) ?: 0)
73+
}
74+
onChange(it)
75+
}
76+
77+
spinner.onItemSelectedListener =
78+
object : AdapterView.OnItemSelectedListener {
79+
override fun onItemSelected(
80+
parent: AdapterView<*>,
81+
view: View?,
82+
position: Int,
83+
id: Long
84+
) {
85+
liveData.value = parent.getItemAtPosition(position) as? String
86+
}
87+
88+
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
89+
}
90+
}
91+
92+
private fun SpinnerAdapter.findItemPosition(item: Any): Int? {
93+
for (pos in 0..count) {
94+
if (item == getItem(pos)) return pos
95+
}
96+
return null
97+
}
98+
}
Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,117 @@
1-
21
package com.mapbox.navigation.examples.androidauto.app
32

43
import android.os.Bundle
5-
import androidx.appcompat.app.AppCompatActivity
4+
import android.view.View
5+
import androidx.appcompat.widget.AppCompatImageView
6+
import androidx.lifecycle.lifecycleScope
7+
import com.mapbox.api.directions.v5.models.BannerInstructions
8+
import com.mapbox.common.LogConfiguration
9+
import com.mapbox.common.LoggingLevel
10+
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
11+
import com.mapbox.navigation.core.MapboxNavigation
12+
import com.mapbox.navigation.core.internal.extensions.flowRoutesUpdated
13+
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
14+
import com.mapbox.navigation.core.trip.session.BannerInstructionsObserver
615
import com.mapbox.navigation.examples.androidauto.CarAppSyncComponent
7-
import com.mapbox.navigation.examples.androidauto.databinding.MapboxActivityNavigationViewBinding
16+
import com.mapbox.navigation.examples.androidauto.databinding.ActivityMainBinding
17+
import com.mapbox.navigation.examples.androidauto.databinding.LayoutDrawerMenuNavViewBinding
18+
import com.mapbox.navigation.examples.androidauto.utils.NavigationViewController
19+
import com.mapbox.navigation.examples.androidauto.utils.TestRoutes
20+
import com.mapbox.navigation.ui.base.installer.installComponents
21+
import com.mapbox.navigation.ui.base.lifecycle.UIComponent
22+
import com.mapbox.navigation.ui.maps.guidance.junction.api.MapboxJunctionApi
23+
import kotlinx.coroutines.ExperimentalCoroutinesApi
24+
import kotlinx.coroutines.channels.awaitClose
25+
import kotlinx.coroutines.flow.Flow
26+
import kotlinx.coroutines.flow.callbackFlow
27+
import kotlinx.coroutines.launch
28+
29+
class MainActivity : DrawerActivity() {
30+
31+
private lateinit var binding: ActivityMainBinding
32+
private lateinit var menuBinding: LayoutDrawerMenuNavViewBinding
833

9-
class MainActivity : AppCompatActivity() {
10-
private lateinit var binding: MapboxActivityNavigationViewBinding
34+
override fun onCreateContentView(): View {
35+
binding = ActivityMainBinding.inflate(layoutInflater)
36+
CarAppSyncComponent.getInstance().attachNavigationView(binding.navigationView)
37+
return binding.root
38+
}
1139

40+
override fun onCreateMenuView(): View {
41+
menuBinding = LayoutDrawerMenuNavViewBinding.inflate(layoutInflater)
42+
return menuBinding.root
43+
}
44+
45+
private lateinit var controller: NavigationViewController
46+
47+
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
1248
override fun onCreate(savedInstanceState: Bundle?) {
1349
super.onCreate(savedInstanceState)
14-
binding = MapboxActivityNavigationViewBinding.inflate(layoutInflater)
15-
setContentView(binding.root)
1650

17-
// TODO going to expose a public api to share a replay controller
18-
// This allows to simulate your location
19-
// binding.navigationView.api.routeReplayEnabled(true)
51+
LogConfiguration.setLoggingLevel("nav-sdk", LoggingLevel.DEBUG)
2052

21-
CarAppSyncComponent.getInstance().attachNavigationView(binding.navigationView)
53+
controller = NavigationViewController(this, binding.navigationView)
54+
55+
menuBinding.toggleReplay.isChecked = binding.navigationView.api.isReplayEnabled()
56+
menuBinding.toggleReplay.setOnCheckedChangeListener { _, isChecked ->
57+
binding.navigationView.api.routeReplayEnabled(isChecked)
58+
}
59+
60+
menuBinding.junctionViewTestButton.setOnClickListener {
61+
lifecycleScope.launch {
62+
val (origin, destination) = TestRoutes.valueOf(
63+
menuBinding.spinnerTestRoute.selectedItem as String
64+
)
65+
controller.startActiveGuidance(origin, destination)
66+
closeDrawers()
67+
}
68+
}
69+
70+
MapboxNavigationApp.installComponents(this) {
71+
component(Junctions(binding.junctionImageView))
72+
}
73+
}
74+
75+
/**
76+
* Simple component for detecting and rendering Junction Views.
77+
*/
78+
private class Junctions(
79+
private val imageView: AppCompatImageView
80+
) : UIComponent() {
81+
private var junctionApi: MapboxJunctionApi? = null
82+
83+
override fun onAttached(mapboxNavigation: MapboxNavigation) {
84+
super.onAttached(mapboxNavigation)
85+
val token = mapboxNavigation.navigationOptions.accessToken!!
86+
junctionApi = MapboxJunctionApi(token)
87+
88+
mapboxNavigation.flowBannerInstructions().observe { instructions ->
89+
junctionApi?.generateJunction(instructions) { result ->
90+
result.fold(
91+
{ imageView.setImageBitmap(null) },
92+
{ imageView.setImageBitmap(it.bitmap) }
93+
)
94+
}
95+
}
96+
mapboxNavigation.flowRoutesUpdated().observe {
97+
if (it.navigationRoutes.isEmpty()) {
98+
imageView.setImageBitmap(null)
99+
}
100+
}
101+
}
102+
103+
override fun onDetached(mapboxNavigation: MapboxNavigation) {
104+
super.onDetached(mapboxNavigation)
105+
junctionApi?.cancelAll()
106+
junctionApi = null
107+
}
108+
109+
@OptIn(ExperimentalCoroutinesApi::class)
110+
private fun MapboxNavigation.flowBannerInstructions(): Flow<BannerInstructions> =
111+
callbackFlow {
112+
val observer = BannerInstructionsObserver { trySend(it) }
113+
registerBannerInstructionsObserver(observer)
114+
awaitClose { unregisterBannerInstructionsObserver(observer) }
115+
}
22116
}
23117
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.mapbox.navigation.examples.androidauto.utils
2+
3+
import com.mapbox.api.directions.v5.models.RouteOptions
4+
import com.mapbox.geojson.Point
5+
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
6+
import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions
7+
import com.mapbox.navigation.base.route.NavigationRoute
8+
import com.mapbox.navigation.base.route.NavigationRouterCallback
9+
import com.mapbox.navigation.base.route.RouterFailure
10+
import com.mapbox.navigation.base.route.RouterOrigin
11+
import com.mapbox.navigation.core.MapboxNavigation
12+
import kotlinx.coroutines.suspendCancellableCoroutine
13+
import kotlin.coroutines.resume
14+
import kotlin.coroutines.resumeWithException
15+
16+
internal suspend fun MapboxNavigation.fetchRoute(
17+
origin: Point,
18+
destination: Point,
19+
): List<NavigationRoute> =
20+
fetchRoute(
21+
RouteOptions.builder()
22+
.applyDefaultNavigationOptions()
23+
.applyLanguageAndVoiceUnitOptions(navigationOptions.applicationContext)
24+
.layersList(listOf(getZLevel(), null))
25+
.coordinatesList(listOf(origin, destination))
26+
.alternatives(true)
27+
.build()
28+
)
29+
30+
internal suspend fun MapboxNavigation.fetchRoute(
31+
routeOptions: RouteOptions
32+
): List<NavigationRoute> = suspendCancellableCoroutine { cont ->
33+
val requestId = requestRoutes(
34+
routeOptions,
35+
object : NavigationRouterCallback {
36+
override fun onRoutesReady(
37+
routes: List<NavigationRoute>,
38+
routerOrigin: RouterOrigin
39+
) {
40+
cont.resume(routes)
41+
}
42+
43+
override fun onFailure(
44+
reasons: List<RouterFailure>,
45+
routeOptions: RouteOptions
46+
) {
47+
cont.resumeWithException(FetchRouteError(reasons, routeOptions))
48+
}
49+
50+
override fun onCanceled(
51+
routeOptions: RouteOptions,
52+
routerOrigin: RouterOrigin
53+
) = Unit
54+
}
55+
)
56+
cont.invokeOnCancellation { cancelRouteRequest(requestId) }
57+
}
58+
59+
internal class FetchRouteError(
60+
val reasons: List<RouterFailure>,
61+
val routeOptions: RouteOptions
62+
) : Error()
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.mapbox.navigation.examples.androidauto.utils
2+
3+
import android.location.Location
4+
import androidx.lifecycle.DefaultLifecycleObserver
5+
import androidx.lifecycle.LifecycleOwner
6+
import com.mapbox.geojson.Point
7+
import com.mapbox.navigation.base.route.NavigationRoute
8+
import com.mapbox.navigation.core.MapboxNavigation
9+
import com.mapbox.navigation.core.internal.extensions.flowNewRawLocation
10+
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
11+
import com.mapbox.navigation.dropin.NavigationView
12+
import com.mapbox.navigation.ui.base.lifecycle.UIComponent
13+
import com.mapbox.navigation.utils.internal.toPoint
14+
import kotlinx.coroutines.flow.MutableStateFlow
15+
import kotlinx.coroutines.flow.filterNotNull
16+
import kotlinx.coroutines.flow.first
17+
18+
/**
19+
* Lifecycle aware thin wrapper around NavigationView that offers convenience methods for
20+
* fetching routes and starting active navigation.
21+
*/
22+
internal class NavigationViewController(
23+
lifecycleOwner: LifecycleOwner,
24+
private val navigationView: NavigationView
25+
) : DefaultLifecycleObserver, UIComponent() {
26+
init {
27+
lifecycleOwner.lifecycle.addObserver(this)
28+
}
29+
30+
val location = MutableStateFlow<Location?>(null)
31+
private val mapboxNavigation = MutableStateFlow<MapboxNavigation?>(null)
32+
33+
override fun onCreate(owner: LifecycleOwner) {
34+
MapboxNavigationApp.registerObserver(this)
35+
}
36+
37+
override fun onDestroy(owner: LifecycleOwner) {
38+
MapboxNavigationApp.unregisterObserver(this)
39+
}
40+
41+
override fun onAttached(mapboxNavigation: MapboxNavigation) {
42+
super.onAttached(mapboxNavigation)
43+
this.mapboxNavigation.value = mapboxNavigation
44+
mapboxNavigation.flowNewRawLocation().observe {
45+
location.value = it
46+
}
47+
}
48+
49+
override fun onDetached(mapboxNavigation: MapboxNavigation) {
50+
super.onDetached(mapboxNavigation)
51+
this.mapboxNavigation.value = null
52+
}
53+
54+
suspend fun startActiveGuidance(destination: Point) {
55+
val routes = fetchRoute(destination)
56+
navigationView.api.startActiveGuidance(routes)
57+
}
58+
59+
suspend fun startActiveGuidance(origin: Point, destination: Point) {
60+
val routes = fetchRoute(origin, destination)
61+
navigationView.api.startActiveGuidance(routes)
62+
}
63+
64+
suspend fun fetchRoute(destination: Point): List<NavigationRoute> {
65+
val origin = location.filterNotNull().first().toPoint()
66+
return fetchRoute(origin, destination)
67+
}
68+
69+
suspend fun fetchRoute(origin: Point, destination: Point): List<NavigationRoute> {
70+
val mapboxNavigation = this.mapboxNavigation.filterNotNull().first()
71+
return mapboxNavigation.fetchRoute(origin, destination)
72+
}
73+
}

0 commit comments

Comments
 (0)