Skip to content

Commit

Permalink
[Android Auto] Add support for Junction Views (#6849)
Browse files Browse the repository at this point in the history
* Android Auto - added support for Junction Views
- CarLanesImageRenderer optimization. Cleanup.
  • Loading branch information
tomaszrybakiewicz authored Jan 20, 2023
1 parent e0c5710 commit 5f67450
Show file tree
Hide file tree
Showing 23 changed files with 1,057 additions and 50 deletions.
7 changes: 7 additions & 0 deletions android-auto-app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
1 change: 1 addition & 0 deletions android-auto-app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
android:theme="@style/Theme.MapboxNavigationExamples">
<activity
android:name=".app.MainActivity"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Boolean>,
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<String>,
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
}
}
Original file line number Diff line number Diff line change
@@ -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<BannerInstructions> =
callbackFlow {
val observer = BannerInstructionsObserver { trySend(it) }
registerBannerInstructionsObserver(observer)
awaitClose { unregisterBannerInstructionsObserver(observer) }
}
}
}
Original file line number Diff line number Diff line change
@@ -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<NavigationRoute> =
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<NavigationRoute> = suspendCancellableCoroutine { cont ->
val requestId = requestRoutes(
routeOptions,
object : NavigationRouterCallback {
override fun onRoutesReady(
routes: List<NavigationRoute>,
routerOrigin: RouterOrigin
) {
cont.resume(routes)
}

override fun onFailure(
reasons: List<RouterFailure>,
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<RouterFailure>,
val routeOptions: RouteOptions
) : Error()
Original file line number Diff line number Diff line change
@@ -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<Location?>(null)
private val mapboxNavigation = MutableStateFlow<MapboxNavigation?>(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<NavigationRoute> {
val origin = location.filterNotNull().first().toPoint()
return fetchRoute(origin, destination)
}

suspend fun fetchRoute(origin: Point, destination: Point): List<NavigationRoute> {
val mapboxNavigation = this.mapboxNavigation.filterNotNull().first()
return mapboxNavigation.fetchRoute(origin, destination)
}
}
Loading

0 comments on commit 5f67450

Please sign in to comment.