Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering that junction views will eventually be part of drop-in-ui and this is an android-auto example. I think we should remove the app code so we don't have to maintain it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll remove it once Drop-In UI adds Junction View support.

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