Compare commits
3
Commits
2ce079a7c1
...
b99ebfd36f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b99ebfd36f | ||
|
|
69b27d3b6c | ||
|
|
8b886c36b1 |
@@ -59,7 +59,7 @@ class DeviceLocationManagerService(
|
|||||||
* @param minDistanceM Minimum distance between updates in meters (default: 5m)
|
* @param minDistanceM Minimum distance between updates in meters (default: 5m)
|
||||||
*/
|
*/
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 1.0f) {
|
fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 5.0f) {
|
||||||
if (isListening) return
|
if (isListening) return
|
||||||
|
|
||||||
// Get and deliver last known location first
|
// Get and deliver last known location first
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ class NavigationCarAppService : CarAppService() {
|
|||||||
return ClusterSession()
|
return ClusterSession()
|
||||||
} else {
|
} else {
|
||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
return NavigationSession()
|
//return NavigationSession()
|
||||||
|
return NavigationServiceSession()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,724 @@
|
|||||||
|
package com.kouros.navigation.car
|
||||||
|
|
||||||
|
import android.Manifest.permission
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.location.Location
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresPermission
|
||||||
|
import androidx.car.app.CarContext
|
||||||
|
import androidx.car.app.Screen
|
||||||
|
import androidx.car.app.ScreenManager
|
||||||
|
import androidx.car.app.connection.CarConnection
|
||||||
|
import androidx.car.app.model.CarColor
|
||||||
|
import androidx.car.app.model.CarIcon
|
||||||
|
import androidx.car.app.model.Distance
|
||||||
|
import androidx.car.app.navigation.model.Destination
|
||||||
|
import androidx.car.app.navigation.model.Step
|
||||||
|
import androidx.car.app.navigation.model.TravelEstimate
|
||||||
|
import androidx.car.app.navigation.model.Trip
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.ViewModelStore
|
||||||
|
import androidx.lifecycle.ViewModelStoreOwner
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.kouros.navigation.car.navigation.NavigationService
|
||||||
|
import com.kouros.navigation.car.screen.NavigationListener
|
||||||
|
import com.kouros.navigation.car.screen.NavigationScreen
|
||||||
|
import com.kouros.navigation.car.screen.NavigationType
|
||||||
|
import com.kouros.navigation.car.screen.RequestPermissionScreen
|
||||||
|
import com.kouros.navigation.car.screen.SearchScreen
|
||||||
|
import com.kouros.navigation.car.screen.checkPermission
|
||||||
|
import com.kouros.navigation.car.screen.observers.NavigationObserverCallback
|
||||||
|
import com.kouros.navigation.car.screen.observers.NavigationObserverManager
|
||||||
|
import com.kouros.navigation.data.Constants.AUTOMOTIVE_CAR_SPEED_PERMISSION
|
||||||
|
import com.kouros.navigation.data.Constants.GMS_CAR_SPEED_PERMISSION
|
||||||
|
import com.kouros.navigation.data.Constants.TAG
|
||||||
|
import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE
|
||||||
|
import com.kouros.navigation.data.Constants.homeHohenwaldeck
|
||||||
|
import com.kouros.navigation.data.Place
|
||||||
|
import com.kouros.navigation.data.RouteEngine
|
||||||
|
import com.kouros.navigation.data.ViewStyle
|
||||||
|
import com.kouros.navigation.data.osrm.OsrmRepository
|
||||||
|
import com.kouros.navigation.data.overpass.Elements
|
||||||
|
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||||
|
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
||||||
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
|
import com.kouros.navigation.model.SettingsViewModel
|
||||||
|
import com.kouros.navigation.repository.SettingsRepository
|
||||||
|
import com.kouros.navigation.utils.GeoUtils
|
||||||
|
import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
||||||
|
import com.kouros.navigation.utils.getSettingsRepository
|
||||||
|
import com.kouros.navigation.utils.getSettingsViewModel
|
||||||
|
import com.kouros.navigation.utils.location
|
||||||
|
import kotlinx.coroutines.awaitCancellation
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main session for Android Auto/Automotive OS navigation.
|
||||||
|
* Manages the lifecycle of the navigation session, including location updates,
|
||||||
|
* car hardware sensors, routing engine selection, and screen navigation.
|
||||||
|
* Implements NavigationScreen.Listener for handling navigation events.
|
||||||
|
*/
|
||||||
|
class NavigationServiceSession : CarSession(), NavigationListener, NavigationObserverCallback {
|
||||||
|
|
||||||
|
// Flag to enable/disable contact access feature
|
||||||
|
val useContacts = false
|
||||||
|
|
||||||
|
var route = ""
|
||||||
|
|
||||||
|
// Main navigation screen displayed to the user
|
||||||
|
lateinit var navigationScreen: NavigationScreen
|
||||||
|
|
||||||
|
// Handles map surface rendering on the car display
|
||||||
|
lateinit var surfaceRenderer: SurfaceRenderer
|
||||||
|
|
||||||
|
// Manages car hardware sensors (location, compass, speed)
|
||||||
|
lateinit var carSensorManager: CarSensorManager
|
||||||
|
|
||||||
|
var initialLocation = true;
|
||||||
|
|
||||||
|
lateinit var textToSpeechManager: TextToSpeechManager
|
||||||
|
|
||||||
|
lateinit var notificationManager: NotificationManager
|
||||||
|
|
||||||
|
private var routingEngine = 0
|
||||||
|
|
||||||
|
private var showTraffic = false;
|
||||||
|
|
||||||
|
private var distanceMode = 0
|
||||||
|
var lastCameraSearch = 0
|
||||||
|
|
||||||
|
var speedCameras = listOf<Elements>()
|
||||||
|
|
||||||
|
var recentPlaces = mutableListOf<Place>()
|
||||||
|
|
||||||
|
var lastRouteDate: LocalDateTime = LocalDateTime.now()
|
||||||
|
|
||||||
|
var destination = Place()
|
||||||
|
var notificationActive = false
|
||||||
|
|
||||||
|
var navigationService: NavigationService? = null
|
||||||
|
|
||||||
|
val serviceListener: NavigationService.Listener = object : NavigationService.Listener {
|
||||||
|
|
||||||
|
override fun navigationStateChanged(
|
||||||
|
isNavigating: Boolean,
|
||||||
|
isRerouting: Boolean,
|
||||||
|
hasArrived: Boolean,
|
||||||
|
destinations: MutableList<Destination>,
|
||||||
|
steps: MutableList<Step>,
|
||||||
|
destinationTravelEstimate: TravelEstimate,
|
||||||
|
stepTravelEstimate: TravelEstimate,
|
||||||
|
stepRemainingDistance: Distance,
|
||||||
|
shouldShowNextStep: Boolean,
|
||||||
|
shouldShowLanes: Boolean,
|
||||||
|
junctionImage: CarIcon?,
|
||||||
|
backGroundColor: CarColor
|
||||||
|
) {
|
||||||
|
|
||||||
|
navigationScreen.updateTrip(
|
||||||
|
isNavigating = isNavigating,
|
||||||
|
isRerouting = isRerouting,
|
||||||
|
hasArrived = hasArrived,
|
||||||
|
destinationTravelEstimate = destinationTravelEstimate,
|
||||||
|
stepTravelEstimate = stepTravelEstimate,
|
||||||
|
destinations = destinations,
|
||||||
|
steps = steps,
|
||||||
|
stepRemainingDistance = stepRemainingDistance,
|
||||||
|
shouldShowNextStep = shouldShowNextStep,
|
||||||
|
shouldShowLanes = shouldShowLanes,
|
||||||
|
junctionImage = junctionImage,
|
||||||
|
backGroundColor = backGroundColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateServiceLocation(location: Location) {
|
||||||
|
if (initialLocation) {
|
||||||
|
navigationViewModel.loadRecentPlaces(
|
||||||
|
carContext,
|
||||||
|
location,
|
||||||
|
surfaceRenderer.carOrientation,
|
||||||
|
)
|
||||||
|
initialLocation = false
|
||||||
|
}
|
||||||
|
updateLocation(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitors the state of the connection to the Navigation service.
|
||||||
|
val serviceConnection: ServiceConnection = object : ServiceConnection {
|
||||||
|
@RequiresPermission(allOf = [permission.ACCESS_FINE_LOCATION, permission.ACCESS_COARSE_LOCATION])
|
||||||
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
|
Log.d("NavigationService", "In onServiceConnected() Session component:$service")
|
||||||
|
val binder: NavigationService.LocalBinder = service as NavigationService.LocalBinder
|
||||||
|
navigationService = binder.service
|
||||||
|
navigationService!!.setCarContext(carContext, serviceListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
Log.d("NavigationService", "In onServiceDisconnected() Session component: $name")
|
||||||
|
// Unhook map models here
|
||||||
|
navigationService!!.clearCarContext()
|
||||||
|
navigationService = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle observer for managing session lifecycle events.
|
||||||
|
* Cleans up resources when the session is destroyed.
|
||||||
|
*/
|
||||||
|
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
|
||||||
|
|
||||||
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
|
Log.i(TAG, "In onStart() Session")
|
||||||
|
carContext
|
||||||
|
.bindService(
|
||||||
|
Intent(carContext, NavigationService::class.java),
|
||||||
|
serviceConnection,
|
||||||
|
Context.BIND_AUTO_CREATE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause(owner: LifecycleOwner) {
|
||||||
|
Log.d(TAG, "NavigationSession paused")
|
||||||
|
super.onPause(owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume(owner: LifecycleOwner) {
|
||||||
|
Log.d(TAG, "NavigationSession resumed")
|
||||||
|
super.onResume(owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
|
Log.i(TAG, "In onStop()")
|
||||||
|
carContext.unbindService(serviceConnection)
|
||||||
|
navigationService = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
if (::carSensorManager.isInitialized) {
|
||||||
|
carSensorManager.cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (::textToSpeechManager.isInitialized) {
|
||||||
|
textToSpeechManager.cleanup()
|
||||||
|
}
|
||||||
|
carContext
|
||||||
|
.stopService(
|
||||||
|
Intent(
|
||||||
|
carContext,
|
||||||
|
NavigationNotificationService::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Log.i(TAG, "NavigationSession destroyed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewModel for navigation data and business logic
|
||||||
|
lateinit var navigationViewModel: NavigationViewModel
|
||||||
|
|
||||||
|
// Store for ViewModels to survive configuration changes
|
||||||
|
lateinit var viewModelStoreOwner: ViewModelStoreOwner
|
||||||
|
|
||||||
|
var lastStepIndex = -1
|
||||||
|
|
||||||
|
var guidanceAudio = 0
|
||||||
|
|
||||||
|
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
|
||||||
|
lateinit var observerManager: NavigationObserverManager
|
||||||
|
|
||||||
|
lateinit var repository: SettingsRepository
|
||||||
|
|
||||||
|
lateinit var settingsViewModel: SettingsViewModel
|
||||||
|
|
||||||
|
var carConnection: Int = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
lifecycle.addObserver(lifecycleObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when routing engine preference changes.
|
||||||
|
* Creates appropriate repository based on user selection.
|
||||||
|
*/
|
||||||
|
fun onRoutingEngineStateUpdated(routeEngine: Int) {
|
||||||
|
Log.d(TAG, "onRoutingEngineStateUpdated $routeEngine")
|
||||||
|
if (!::navigationViewModel.isInitialized || routeEngine != routingEngine) {
|
||||||
|
navigationViewModel = when (routeEngine) {
|
||||||
|
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
|
||||||
|
RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository())
|
||||||
|
else -> NavigationViewModel(TomTomRepository())
|
||||||
|
}
|
||||||
|
observerManager = NavigationObserverManager(navigationViewModel, this)
|
||||||
|
observerManager.attachAllObservers(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when location permission is granted.
|
||||||
|
* Initializes car hardware sensors if available.
|
||||||
|
*/
|
||||||
|
fun onPermissionGranted(permission: Boolean) {
|
||||||
|
if (::carSensorManager.isInitialized && permission) {
|
||||||
|
carSensorManager.updateConnectionState(carConnection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when car connection state changes.
|
||||||
|
* Handles different connection types: Not Connected, Automotive OS Native, Android Auto Projection.
|
||||||
|
* Requests appropriate car speed permissions based on connection type.
|
||||||
|
*/
|
||||||
|
fun onConnectionStateUpdated(connectionState: Int) {
|
||||||
|
carConnection = connectionState
|
||||||
|
when (connectionState) {
|
||||||
|
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit
|
||||||
|
CarConnection.CONNECTION_TYPE_NATIVE -> {
|
||||||
|
navigationViewModel.permissionGranted.value =
|
||||||
|
checkPermission(carContext, AUTOMOTIVE_CAR_SPEED_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
CarConnection.CONNECTION_TYPE_PROJECTION -> {
|
||||||
|
navigationViewModel.permissionGranted.value =
|
||||||
|
checkPermission(carContext, GMS_CAR_SPEED_PERMISSION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the initial screen for the session.
|
||||||
|
* Sets up ViewModel store, initializes settings, components, checks permissions,
|
||||||
|
* and returns appropriate starting screen.
|
||||||
|
*/
|
||||||
|
override fun onCreateScreen(intent: Intent): Screen {
|
||||||
|
initializeSettings()
|
||||||
|
setupViewModelStore()
|
||||||
|
initializeManagers()
|
||||||
|
initializeViewModels()
|
||||||
|
initializeScreen()
|
||||||
|
return checkPermissionsAndGetScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initializes the settings repository and ViewModel.
|
||||||
|
*/
|
||||||
|
private fun initializeSettings() {
|
||||||
|
repository = getSettingsRepository(carContext)
|
||||||
|
settingsViewModel = getSettingsViewModel(carContext)
|
||||||
|
|
||||||
|
repository.routingEngineFlow.asLiveData().observe(this, Observer {
|
||||||
|
onRoutingEngineStateUpdated(it)
|
||||||
|
routingEngine = it
|
||||||
|
})
|
||||||
|
|
||||||
|
repository.trafficFlow.asLiveData().observe(this, Observer {
|
||||||
|
showTraffic = it
|
||||||
|
})
|
||||||
|
repository.distanceModeFlow.asLiveData().observe(this, Observer {
|
||||||
|
distanceMode = it
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up ViewModelStoreOwner and manages its lifecycle.
|
||||||
|
*/
|
||||||
|
private fun setupViewModelStore() {
|
||||||
|
viewModelStoreOwner = object : ViewModelStoreOwner {
|
||||||
|
override val viewModelStore = ViewModelStore()
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
awaitCancellation()
|
||||||
|
} finally {
|
||||||
|
viewModelStoreOwner.viewModelStore.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes ViewModels and observes their state changes.
|
||||||
|
*/
|
||||||
|
private fun initializeViewModels() {
|
||||||
|
navigationViewModel = getViewModel(carContext)
|
||||||
|
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
|
||||||
|
navigationViewModel.permissionGranted.observe(this, ::onPermissionGranted)
|
||||||
|
|
||||||
|
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes managers for rendering, sensors, and location.
|
||||||
|
*/
|
||||||
|
private fun initializeManagers() {
|
||||||
|
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
|
||||||
|
|
||||||
|
carSensorManager = CarSensorManager(
|
||||||
|
carContext = carContext,
|
||||||
|
lifecycleOwner = this,
|
||||||
|
onLocationUpdate = ::updateLocation,
|
||||||
|
onCompassUpdate = { orientation -> surfaceRenderer.carOrientation = orientation },
|
||||||
|
onSpeedUpdate = { speed -> surfaceRenderer.updateCarSpeed(speed) }
|
||||||
|
)
|
||||||
|
|
||||||
|
textToSpeechManager = TextToSpeechManager(carContext)
|
||||||
|
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
|
||||||
|
guidanceAudio = it
|
||||||
|
})
|
||||||
|
notificationManager = NotificationManager(carContext, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the main navigation screen.
|
||||||
|
*/
|
||||||
|
private fun initializeScreen() {
|
||||||
|
navigationScreen = NavigationScreen(
|
||||||
|
carContext,
|
||||||
|
surfaceRenderer,
|
||||||
|
this,
|
||||||
|
navigationViewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks required permissions and returns appropriate screen.
|
||||||
|
* Shows permission request screen if needed, otherwise starts location updates.
|
||||||
|
*/
|
||||||
|
private fun checkPermissionsAndGetScreen(): Screen {
|
||||||
|
val hasLocationPermission =
|
||||||
|
carContext.checkSelfPermission(permission.ACCESS_FINE_LOCATION) ==
|
||||||
|
PackageManager.PERMISSION_GRANTED
|
||||||
|
val hasContactsPermission = !useContacts ||
|
||||||
|
carContext.checkSelfPermission(permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
return if (hasLocationPermission && hasContactsPermission) {
|
||||||
|
navigationScreen
|
||||||
|
} else {
|
||||||
|
showPermissionScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the permission request screen.
|
||||||
|
*/
|
||||||
|
private fun showPermissionScreen(): Screen {
|
||||||
|
val screenManager = carContext.getCarService(ScreenManager::class.java)
|
||||||
|
screenManager.push(navigationScreen)
|
||||||
|
return RequestPermissionScreen(
|
||||||
|
carContext,
|
||||||
|
listOf(
|
||||||
|
permission.ACCESS_COARSE_LOCATION,
|
||||||
|
permission.ACCESS_FINE_LOCATION,
|
||||||
|
),
|
||||||
|
permissionCheckCallback = { screenManager.pop() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles new intents, primarily for navigation deep links from other apps.
|
||||||
|
* Supports ACTION_NAVIGATE for starting navigation to a specific location.
|
||||||
|
*/
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
val screenManager = carContext.getCarService(ScreenManager::class.java)
|
||||||
|
|
||||||
|
// Handle Android Auto ACTION_NAVIGATE intent
|
||||||
|
if (CarContext.ACTION_NAVIGATE == intent.action) {
|
||||||
|
handleNavigateIntent(screenManager)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle custom deep links
|
||||||
|
handleDeepLink(intent, screenManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles ACTION_NAVIGATE intent by showing search screen.
|
||||||
|
*/
|
||||||
|
private fun handleNavigateIntent(screenManager: ScreenManager) {
|
||||||
|
screenManager.popToRoot()
|
||||||
|
screenManager.pushForResult(
|
||||||
|
SearchScreen(carContext, surfaceRenderer, navigationViewModel, recentPlaces)
|
||||||
|
) { result ->
|
||||||
|
// Handle search result if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles custom deep link URIs.
|
||||||
|
*/
|
||||||
|
private fun handleDeepLink(intent: Intent, screenManager: ScreenManager) {
|
||||||
|
val uri = intent.data ?: return
|
||||||
|
if (uri.scheme != uriScheme || uri.schemeSpecificPart != uriHost) return
|
||||||
|
|
||||||
|
when (uri.fragment) {
|
||||||
|
"DEEP_LINK_ACTION" -> {
|
||||||
|
if (screenManager.getTop() !is NavigationScreen) {
|
||||||
|
screenManager.popToRoot()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates navigation state with new location.
|
||||||
|
* Handles route snapping, deviation detection for rerouting, and map updates.
|
||||||
|
*/
|
||||||
|
fun updateLocation(location: Location) {
|
||||||
|
|
||||||
|
if (carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) {
|
||||||
|
surfaceRenderer.updateCarSpeed(location.speed)
|
||||||
|
}
|
||||||
|
updateBearing(location)
|
||||||
|
checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location)
|
||||||
|
surfaceRenderer.updateLocation(location, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates route bearing if location has bearing information.
|
||||||
|
*/
|
||||||
|
private fun updateBearing(location: Location) {
|
||||||
|
if (location.hasBearing()) {
|
||||||
|
//routeModel.navState = routeModel.navState.copy(routeBearing = location.bearing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start navigation process.
|
||||||
|
* Called when user starts navigation
|
||||||
|
*/
|
||||||
|
override fun startNavigation() {
|
||||||
|
Log.d(TAG, "startNavigation")
|
||||||
|
navigationService!!.startNavigation(route, destination)
|
||||||
|
if (notificationActive)
|
||||||
|
notificationManager.startNotificationService()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops active navigation and clears route state.
|
||||||
|
* Called when user exits navigation or arrives at destination.
|
||||||
|
*/
|
||||||
|
override fun stopNavigation() {
|
||||||
|
Log.d(TAG, "stopNavigation")
|
||||||
|
navigationService!!.stopNavigation()
|
||||||
|
surfaceRenderer.navigation = false
|
||||||
|
surfaceRenderer.routeData.value = ""
|
||||||
|
lastCameraSearch = 0
|
||||||
|
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||||
|
navigationScreen.navigationType = NavigationType.VIEW
|
||||||
|
if (notificationActive)
|
||||||
|
notificationManager.stopNotificationService()
|
||||||
|
Log.d(TAG, "end stopNavigation")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateTrip(trip: Trip) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recalculates a route for the specified place.
|
||||||
|
*/
|
||||||
|
override fun recalcRoute(destination: Place) {
|
||||||
|
val destination = location(destination.longitude, destination.latitude)
|
||||||
|
navigationViewModel.loadRoute(
|
||||||
|
carContext,
|
||||||
|
surfaceRenderer.lastLocation,
|
||||||
|
listOf(destination),
|
||||||
|
surfaceRenderer.carOrientation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the received route string.
|
||||||
|
* Starts navigation and invalidates the screen.
|
||||||
|
*/
|
||||||
|
override fun onRouteReceived(route: String) {
|
||||||
|
Log.d(TAG, "onRouteReceived")
|
||||||
|
if (route.isNotEmpty()) {
|
||||||
|
prepareRoute(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isNavigating(): Boolean {
|
||||||
|
return navigationService!!.isNavigating()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare route and start navigation
|
||||||
|
*/
|
||||||
|
private fun prepareRoute(route: String) {
|
||||||
|
this.route = route
|
||||||
|
startNavigation()
|
||||||
|
surfaceRenderer.setRouteData(navigationService!!.routeModel.curRoute.routeGeoJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles received traffic data and updates the surface renderer.
|
||||||
|
*/
|
||||||
|
override fun onTrafficReceived(traffic: Map<String, String>) {
|
||||||
|
if (traffic.isNotEmpty()) {
|
||||||
|
surfaceRenderer.setTrafficData(traffic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the received place search result.
|
||||||
|
* Navigates to the specified place.
|
||||||
|
*/
|
||||||
|
override fun onPlaceSearchResultReceived(place: Place) {
|
||||||
|
navigateToPlace(place)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles received speed camera data.
|
||||||
|
* Updates the surface renderer with the camera locations.
|
||||||
|
*/
|
||||||
|
override fun onSpeedCamerasReceived(cameras: List<Elements>) {
|
||||||
|
speedCameras = cameras
|
||||||
|
val coordinates = mutableListOf<List<Double>>()
|
||||||
|
cameras.forEach {
|
||||||
|
coordinates.add(listOf(it.lon, it.lat))
|
||||||
|
}
|
||||||
|
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
|
||||||
|
surfaceRenderer.speedCameraData.value = speedData
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles received maximum speed data and updates the surface renderer.
|
||||||
|
*/
|
||||||
|
override fun onMaxSpeedReceived(speed: Int) {
|
||||||
|
surfaceRenderer.maxSpeed.value = speed
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecentPlacesReceived(places: List<Place>) {
|
||||||
|
Log.d(TAG, "onRecentPlacesReceived ${places.size}")
|
||||||
|
recentPlaces = places.toMutableList()
|
||||||
|
navigationScreen.recentPlaces = places.toMutableList()
|
||||||
|
navigationScreen.invalidate()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidateScreen() {
|
||||||
|
navigationScreen.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a route to the specified place and sets it as the destination.
|
||||||
|
*/
|
||||||
|
override fun navigateToPlace(place: Place) {
|
||||||
|
Log.d(TAG, "navigateToPlace ${place.street}")
|
||||||
|
var prevDestination = Place()
|
||||||
|
if (surfaceRenderer.navigation) {
|
||||||
|
prevDestination = place
|
||||||
|
stopNavigation()
|
||||||
|
}
|
||||||
|
val preview = place.route
|
||||||
|
navigationViewModel.previewRoute.value = ""
|
||||||
|
val location = if (place.stopOver && prevDestination.latitude != 0.0) {
|
||||||
|
listOf(
|
||||||
|
location(place.longitude, place.latitude),
|
||||||
|
location(prevDestination.longitude, prevDestination.latitude)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(location(place.longitude, place.latitude))
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationViewModel.saveRecent(carContext, place)
|
||||||
|
destination = place
|
||||||
|
// routeModel.navState = routeModel.navState.copy(destination = place)
|
||||||
|
if (preview.isEmpty()) {
|
||||||
|
navigationViewModel.loadRoute(
|
||||||
|
carContext,
|
||||||
|
surfaceRenderer.lastLocation,
|
||||||
|
location,
|
||||||
|
surfaceRenderer.carOrientation
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
//routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex)
|
||||||
|
onRouteReceived(preview)
|
||||||
|
}
|
||||||
|
surfaceRenderer.activateNavigationView()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if traffic data needs to be updated based on the time since the last update.
|
||||||
|
*/
|
||||||
|
fun checkTraffic(current: LocalDateTime, location: Location) {
|
||||||
|
val duration = Duration.between(current, lastTrafficDate)
|
||||||
|
if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
|
||||||
|
lastTrafficDate = current
|
||||||
|
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Periodically requests speed camera information near the current location.
|
||||||
|
*/
|
||||||
|
private fun updateSpeedCamera(location: Location) {
|
||||||
|
if (lastCameraSearch++ % 100 == 0) {
|
||||||
|
navigationViewModel.getSpeedCameras(location, 5.0)
|
||||||
|
}
|
||||||
|
if (speedCameras.isNotEmpty()) {
|
||||||
|
updateDistance(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates distances to nearby speed cameras and checks for proximity alerts.
|
||||||
|
*/
|
||||||
|
private fun updateDistance(
|
||||||
|
location: Location,
|
||||||
|
) {
|
||||||
|
val updatedCameras = mutableListOf<Elements>()
|
||||||
|
speedCameras.forEach {
|
||||||
|
val plLocation =
|
||||||
|
location(longitude = it.lon, latitude = it.lat)
|
||||||
|
val distance = plLocation.distanceTo(location)
|
||||||
|
it.distance = distance.toDouble()
|
||||||
|
updatedCameras.add(it)
|
||||||
|
}
|
||||||
|
val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
|
||||||
|
val camera = sortedList.firstOrNull() ?: return
|
||||||
|
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
|
||||||
|
val bearingSpeedCamera = if (camera.tags.direction != null) {
|
||||||
|
try {
|
||||||
|
camera.tags.direction!!.toFloat()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
0F
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
|
||||||
|
}
|
||||||
|
if (camera.distance < 80) {
|
||||||
|
if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) {
|
||||||
|
// routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidateNavigationScreen() {
|
||||||
|
navigationScreen.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// URI host for deep linking
|
||||||
|
var uriHost: String = "navigation"
|
||||||
|
|
||||||
|
// URI scheme for deep linking
|
||||||
|
var uriScheme: String = "samples"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,20 @@
|
|||||||
package com.kouros.navigation.car
|
package com.kouros.navigation.car
|
||||||
|
|
||||||
import android.Manifest.permission
|
import android.Manifest.permission
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.os.IBinder
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresPermission
|
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.CarToast
|
import androidx.car.app.CarToast
|
||||||
import androidx.car.app.Screen
|
import androidx.car.app.Screen
|
||||||
import androidx.car.app.ScreenManager
|
import androidx.car.app.ScreenManager
|
||||||
import androidx.car.app.Session
|
|
||||||
import androidx.car.app.connection.CarConnection
|
import androidx.car.app.connection.CarConnection
|
||||||
import androidx.car.app.model.CarIcon
|
|
||||||
import androidx.car.app.model.Distance
|
import androidx.car.app.model.Distance
|
||||||
import androidx.car.app.navigation.NavigationManager
|
import androidx.car.app.navigation.NavigationManager
|
||||||
import androidx.car.app.navigation.NavigationManagerCallback
|
import androidx.car.app.navigation.NavigationManagerCallback
|
||||||
import androidx.car.app.navigation.model.Destination
|
import androidx.car.app.navigation.model.Destination
|
||||||
import androidx.car.app.navigation.model.Step
|
import androidx.car.app.navigation.model.Step
|
||||||
import androidx.car.app.navigation.model.TravelEstimate
|
|
||||||
import androidx.car.app.navigation.model.Trip
|
import androidx.car.app.navigation.model.Trip
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleObserver
|
||||||
@@ -33,7 +25,6 @@ import androidx.lifecycle.ViewModelStoreOwner
|
|||||||
import androidx.lifecycle.asLiveData
|
import androidx.lifecycle.asLiveData
|
||||||
import androidx.lifecycle.coroutineScope
|
import androidx.lifecycle.coroutineScope
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.kouros.navigation.car.navigation.NavigationService
|
|
||||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||||
import com.kouros.navigation.car.navigation.Simulation
|
import com.kouros.navigation.car.navigation.Simulation
|
||||||
import com.kouros.navigation.car.screen.NavigationListener
|
import com.kouros.navigation.car.screen.NavigationListener
|
||||||
@@ -104,12 +95,9 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
|||||||
lateinit var carSensorManager: CarSensorManager
|
lateinit var carSensorManager: CarSensorManager
|
||||||
|
|
||||||
// Manages device GPS location updates
|
// Manages device GPS location updates
|
||||||
val useDeviceLocationManager = false
|
|
||||||
lateinit var deviceLocationManager: DeviceLocationManager
|
lateinit var deviceLocationManager: DeviceLocationManager
|
||||||
|
|
||||||
var initialLocation = true;
|
lateinit var navigationManager: NavigationManager
|
||||||
|
|
||||||
// lateinit var navigationManager: NavigationManager
|
|
||||||
|
|
||||||
lateinit var textToSpeechManager: TextToSpeechManager
|
lateinit var textToSpeechManager: TextToSpeechManager
|
||||||
|
|
||||||
@@ -135,73 +123,12 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
|||||||
|
|
||||||
var notificationActive = false
|
var notificationActive = false
|
||||||
|
|
||||||
var navigationService: NavigationService? = null
|
|
||||||
|
|
||||||
val serviceListener: NavigationService.Listener = object : NavigationService.Listener {
|
|
||||||
|
|
||||||
override fun navigationStateChanged(
|
|
||||||
isNavigating: Boolean,
|
|
||||||
isRerouting: Boolean,
|
|
||||||
hasArrived: Boolean,
|
|
||||||
destinations: MutableList<Destination?>?,
|
|
||||||
steps: MutableList<Step>?,
|
|
||||||
nextDestinationTravelEstimate: TravelEstimate?,
|
|
||||||
nextStepRemainingDistance: Distance?,
|
|
||||||
shouldShowNextStep: Boolean,
|
|
||||||
shouldShowLanes: Boolean,
|
|
||||||
junctionImage: CarIcon?
|
|
||||||
) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateServiceLocation(location: Location) {
|
|
||||||
if (initialLocation) {
|
|
||||||
Log.d(TAG, "RecentPlaces $location")
|
|
||||||
navigationViewModel.loadRecentPlaces(
|
|
||||||
carContext,
|
|
||||||
location,
|
|
||||||
surfaceRenderer.carOrientation,
|
|
||||||
)
|
|
||||||
initialLocation = false
|
|
||||||
}
|
|
||||||
updateLocation(location)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Monitors the state of the connection to the Navigation service.
|
|
||||||
val serviceConnection: ServiceConnection = object : ServiceConnection {
|
|
||||||
@RequiresPermission(allOf = [permission.ACCESS_FINE_LOCATION, permission.ACCESS_COARSE_LOCATION])
|
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
|
||||||
Log.i(TAG, "In onServiceConnected() Session component:$service")
|
|
||||||
val binder: NavigationService.LocalBinder = service as NavigationService.LocalBinder
|
|
||||||
navigationService = binder.service
|
|
||||||
navigationService!!.setCarContext(carContext, serviceListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
|
||||||
Log.i(TAG, "In onServiceDisconnected() component: $name")
|
|
||||||
// Unhook map models here
|
|
||||||
navigationService!!.clearCarContext()
|
|
||||||
navigationService = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lifecycle observer for managing session lifecycle events.
|
* Lifecycle observer for managing session lifecycle events.
|
||||||
* Cleans up resources when the session is destroyed.
|
* Cleans up resources when the session is destroyed.
|
||||||
*/
|
*/
|
||||||
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
|
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
|
||||||
|
|
||||||
override fun onStart(owner: LifecycleOwner) {
|
|
||||||
Log.i(TAG, "In onStart() Session")
|
|
||||||
carContext
|
|
||||||
.bindService(
|
|
||||||
Intent(carContext, NavigationService::class.java),
|
|
||||||
serviceConnection,
|
|
||||||
Context.BIND_AUTO_CREATE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause(owner: LifecycleOwner) {
|
override fun onPause(owner: LifecycleOwner) {
|
||||||
Log.d(TAG, "NavigationSession paused")
|
Log.d(TAG, "NavigationSession paused")
|
||||||
super.onPause(owner)
|
super.onPause(owner)
|
||||||
@@ -212,16 +139,10 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
|||||||
super.onResume(owner)
|
super.onResume(owner)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
|
||||||
Log.i(TAG, "In onStop()")
|
|
||||||
carContext.unbindService(serviceConnection)
|
|
||||||
navigationService = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy(owner: LifecycleOwner) {
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
// if (::navigationManager.isInitialized) {
|
if (::navigationManager.isInitialized) {
|
||||||
// navigationManager.clearNavigationManagerCallback()
|
navigationManager.clearNavigationManagerCallback()
|
||||||
// }
|
}
|
||||||
if (::carSensorManager.isInitialized) {
|
if (::carSensorManager.isInitialized) {
|
||||||
carSensorManager.cleanup()
|
carSensorManager.cleanup()
|
||||||
}
|
}
|
||||||
@@ -268,7 +189,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
|||||||
* Creates appropriate repository based on user selection.
|
* Creates appropriate repository based on user selection.
|
||||||
*/
|
*/
|
||||||
fun onRoutingEngineStateUpdated(routeEngine: Int) {
|
fun onRoutingEngineStateUpdated(routeEngine: Int) {
|
||||||
Log.d(TAG, "onRoutingEngineStateUpdated $routeEngine")
|
|
||||||
if (!::navigationViewModel.isInitialized || routeEngine != routingEngine) {
|
if (!::navigationViewModel.isInitialized || routeEngine != routingEngine) {
|
||||||
navigationViewModel = when (routeEngine) {
|
navigationViewModel = when (routeEngine) {
|
||||||
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
|
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
|
||||||
@@ -379,26 +299,26 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
|||||||
* Initializes managers for rendering, sensors, and location.
|
* Initializes managers for rendering, sensors, and location.
|
||||||
*/
|
*/
|
||||||
private fun initializeManagers() {
|
private fun initializeManagers() {
|
||||||
// navigationManager = carContext.getCarService(NavigationManager::class.java)
|
navigationManager = carContext.getCarService(NavigationManager::class.java)
|
||||||
// navigationManager.setNavigationManagerCallback(object : NavigationManagerCallback {
|
navigationManager.setNavigationManagerCallback(object : NavigationManagerCallback {
|
||||||
// override fun onAutoDriveEnabled() {
|
override fun onAutoDriveEnabled() {
|
||||||
// // Called when the app should simulate navigation (e.g., for testing)
|
// Called when the app should simulate navigation (e.g., for testing)
|
||||||
// //deviceLocationManager.stopLocationUpdates()
|
deviceLocationManager.stopLocationUpdates()
|
||||||
// autoDriveEnabled = true
|
autoDriveEnabled = true
|
||||||
// startNavigation()
|
startNavigation()
|
||||||
// CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG)
|
CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG)
|
||||||
// .show()
|
.show()
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// override fun onStopNavigation() {
|
override fun onStopNavigation() {
|
||||||
// // Called when the user stops navigation in the car screen
|
// Called when the user stops navigation in the car screen
|
||||||
// // Stop turn-by-turn logic and clean up
|
// Stop turn-by-turn logic and clean up
|
||||||
// stopNavigation()
|
stopNavigation()
|
||||||
// if (autoDriveEnabled) {
|
if (autoDriveEnabled) {
|
||||||
// //deviceLocationManager.startLocationUpdates()
|
deviceLocationManager.startLocationUpdates()
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// })
|
})
|
||||||
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
|
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
|
||||||
|
|
||||||
carSensorManager = CarSensorManager(
|
carSensorManager = CarSensorManager(
|
||||||
@@ -409,21 +329,19 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
|||||||
onSpeedUpdate = { speed -> surfaceRenderer.updateCarSpeed(speed) }
|
onSpeedUpdate = { speed -> surfaceRenderer.updateCarSpeed(speed) }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (useDeviceLocationManager) {
|
deviceLocationManager = DeviceLocationManager(
|
||||||
deviceLocationManager = DeviceLocationManager(
|
carContext = carContext,
|
||||||
carContext = carContext,
|
lifecycleOwner = this,
|
||||||
lifecycleOwner = this,
|
shouldUseCarLocationFlow = carSensorManager.shouldUseCarLocation(),
|
||||||
shouldUseCarLocationFlow = carSensorManager.shouldUseCarLocation(),
|
onLocationUpdate = ::updateLocation,
|
||||||
onLocationUpdate = ::updateLocation,
|
onInitialLocation = { location ->
|
||||||
onInitialLocation = { location ->
|
navigationViewModel.loadRecentPlaces(
|
||||||
navigationViewModel.loadRecentPlaces(
|
carContext,
|
||||||
carContext,
|
location,
|
||||||
location,
|
surfaceRenderer.carOrientation,
|
||||||
surfaceRenderer.carOrientation,
|
)
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
textToSpeechManager = TextToSpeechManager(carContext)
|
textToSpeechManager = TextToSpeechManager(carContext)
|
||||||
@@ -457,8 +375,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
|||||||
carContext.checkSelfPermission(permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
|
carContext.checkSelfPermission(permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
return if (hasLocationPermission && hasContactsPermission) {
|
return if (hasLocationPermission && hasContactsPermission) {
|
||||||
if (useDeviceLocationManager)
|
deviceLocationManager.startLocationUpdates()
|
||||||
deviceLocationManager.startLocationUpdates()
|
|
||||||
navigationScreen
|
navigationScreen
|
||||||
} else {
|
} else {
|
||||||
showPermissionScreen()
|
showPermissionScreen()
|
||||||
@@ -532,7 +449,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
|||||||
* Handles route snapping, deviation detection for rerouting, and map updates.
|
* Handles route snapping, deviation detection for rerouting, and map updates.
|
||||||
*/
|
*/
|
||||||
fun updateLocation(location: Location) {
|
fun updateLocation(location: Location) {
|
||||||
Log.d(TAG, "update location $location")
|
|
||||||
val streetName = if (routeModel.isNavigating()) {
|
val streetName = if (routeModel.isNavigating()) {
|
||||||
routeModel.currentStep().street
|
routeModel.currentStep().street
|
||||||
} else {
|
} else {
|
||||||
@@ -681,13 +597,10 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
|||||||
* Called when user starts navigation
|
* Called when user starts navigation
|
||||||
*/
|
*/
|
||||||
override fun startNavigation() {
|
override fun startNavigation() {
|
||||||
if (useDeviceLocationManager)
|
|
||||||
deviceLocationManager.stopLocationUpdates()
|
|
||||||
Log.d(TAG, "startNavigation")
|
Log.d(TAG, "startNavigation")
|
||||||
navigationService!!.startNavigation()
|
|
||||||
surfaceRenderer.navigation = true
|
surfaceRenderer.navigation = true
|
||||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||||
// navigationManager.navigationStarted()
|
navigationManager.navigationStarted()
|
||||||
navigationManagerStarted = true
|
navigationManagerStarted = true
|
||||||
if (autoDriveEnabled) {
|
if (autoDriveEnabled) {
|
||||||
simulation.startSimulation(
|
simulation.startSimulation(
|
||||||
@@ -706,12 +619,9 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
|||||||
*/
|
*/
|
||||||
override fun stopNavigation() {
|
override fun stopNavigation() {
|
||||||
Log.d(TAG, "stopNavigation")
|
Log.d(TAG, "stopNavigation")
|
||||||
if (useDeviceLocationManager)
|
|
||||||
deviceLocationManager.startLocationUpdates()
|
|
||||||
navigationService!!.stopNavigation()
|
|
||||||
surfaceRenderer.navigation = false
|
surfaceRenderer.navigation = false
|
||||||
routeModel.stopNavigation()
|
routeModel.stopNavigation()
|
||||||
//navigationManager.navigationEnded()
|
navigationManager.navigationEnded()
|
||||||
if (autoDriveEnabled) {
|
if (autoDriveEnabled) {
|
||||||
simulation.stopSimulation()
|
simulation.stopSimulation()
|
||||||
autoDriveEnabled = false
|
autoDriveEnabled = false
|
||||||
@@ -722,12 +632,11 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
|||||||
navigationScreen.navigationType = NavigationType.VIEW
|
navigationScreen.navigationType = NavigationType.VIEW
|
||||||
if (notificationActive)
|
if (notificationActive)
|
||||||
notificationManager.stopNotificationService()
|
notificationManager.stopNotificationService()
|
||||||
Log.d(TAG, "end stopNavigation")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateTrip(trip: Trip) {
|
override fun updateTrip(trip: Trip) {
|
||||||
if (navigationManagerStarted) {
|
if (navigationManagerStarted) {
|
||||||
//navigationManager.updateTrip(trip)
|
navigationManager.updateTrip(trip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -843,6 +752,9 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
|||||||
surfaceRenderer.maxSpeed.value = speed
|
surfaceRenderer.maxSpeed.value = speed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRecentPlacesReceived(places: List<Place>) {
|
||||||
|
}
|
||||||
|
|
||||||
override fun invalidateScreen() {
|
override fun invalidateScreen() {
|
||||||
navigationScreen.invalidate()
|
navigationScreen.invalidate()
|
||||||
}
|
}
|
||||||
|
|||||||
+137
-43
@@ -9,24 +9,40 @@ import android.location.Location
|
|||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.SystemClock
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresPermission
|
import androidx.annotation.RequiresPermission
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.CarToast
|
import androidx.car.app.CarToast
|
||||||
|
import androidx.car.app.model.CarColor
|
||||||
import androidx.car.app.model.CarIcon
|
import androidx.car.app.model.CarIcon
|
||||||
import androidx.car.app.model.Distance
|
import androidx.car.app.model.Distance
|
||||||
|
import androidx.car.app.model.Distance.UNIT_METERS
|
||||||
import androidx.car.app.navigation.NavigationManager
|
import androidx.car.app.navigation.NavigationManager
|
||||||
import androidx.car.app.navigation.NavigationManagerCallback
|
import androidx.car.app.navigation.NavigationManagerCallback
|
||||||
import androidx.car.app.navigation.model.Destination
|
import androidx.car.app.navigation.model.Destination
|
||||||
import androidx.car.app.navigation.model.Step
|
import androidx.car.app.navigation.model.Step
|
||||||
import androidx.car.app.navigation.model.TravelEstimate
|
import androidx.car.app.navigation.model.TravelEstimate
|
||||||
|
import androidx.car.app.navigation.model.Trip
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
import com.kouros.navigation.car.DeviceLocationManagerService
|
import com.kouros.navigation.car.DeviceLocationManagerService
|
||||||
import com.kouros.navigation.data.tomtom.TomTomRepository
|
import com.kouros.navigation.car.screen.NavigationType
|
||||||
import io.ticofab.androidgpxparser.parser.GPXParser
|
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
|
||||||
import io.ticofab.androidgpxparser.parser.domain.Gpx
|
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
|
||||||
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
|
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
|
||||||
import org.joda.time.DateTime
|
import com.kouros.navigation.data.Place
|
||||||
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
|
import com.kouros.navigation.model.SettingsViewModel
|
||||||
|
import com.kouros.navigation.repository.SettingsRepository
|
||||||
|
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
||||||
|
import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
||||||
|
import com.kouros.navigation.utils.formattedDistance
|
||||||
|
import com.kouros.navigation.utils.getSettingsRepository
|
||||||
|
import com.kouros.navigation.utils.getSettingsViewModel
|
||||||
|
import com.kouros.navigation.utils.location
|
||||||
|
import kotlin.collections.copy
|
||||||
|
import kotlin.compareTo
|
||||||
|
|
||||||
|
|
||||||
class NavigationService : Service() {
|
class NavigationService : Service() {
|
||||||
@@ -69,6 +85,10 @@ class NavigationService : Service() {
|
|||||||
// Manages device GPS location updates
|
// Manages device GPS location updates
|
||||||
lateinit var deviceLocationManager: DeviceLocationManagerService
|
lateinit var deviceLocationManager: DeviceLocationManagerService
|
||||||
|
|
||||||
|
var currentLocation = location(0.0, 0.0)
|
||||||
|
|
||||||
|
lateinit var navigationViewModel: NavigationViewModel
|
||||||
|
|
||||||
private lateinit var navigationManager: NavigationManager
|
private lateinit var navigationManager: NavigationManager
|
||||||
private var navigationManagerInitialized = false
|
private var navigationManagerInitialized = false
|
||||||
var binder: IBinder = LocalBinder()
|
var binder: IBinder = LocalBinder()
|
||||||
@@ -81,16 +101,19 @@ class NavigationService : Service() {
|
|||||||
isNavigating: Boolean,
|
isNavigating: Boolean,
|
||||||
isRerouting: Boolean,
|
isRerouting: Boolean,
|
||||||
hasArrived: Boolean,
|
hasArrived: Boolean,
|
||||||
destinations: MutableList<Destination?>?,
|
destinations: MutableList<Destination>,
|
||||||
steps: MutableList<Step>?,
|
steps: MutableList<Step>,
|
||||||
nextDestinationTravelEstimate: TravelEstimate?,
|
destinationTravelEstimate: TravelEstimate,
|
||||||
nextStepRemainingDistance: Distance?,
|
stepTravelEstimate: TravelEstimate,
|
||||||
|
stepRemainingDistance: Distance,
|
||||||
shouldShowNextStep: Boolean,
|
shouldShowNextStep: Boolean,
|
||||||
shouldShowLanes: Boolean,
|
shouldShowLanes: Boolean,
|
||||||
junctionImage: CarIcon?
|
junctionImage: CarIcon?,
|
||||||
|
backGroundColor: CarColor
|
||||||
)
|
)
|
||||||
|
|
||||||
fun updateServiceLocation(location: Location)
|
fun updateServiceLocation(location: Location)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,26 +130,32 @@ class NavigationService : Service() {
|
|||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
Log.i(TAG, "In onCreate()");
|
Log.i(TAG, "In onCreate()");
|
||||||
createNotificationChannel();
|
createNotificationChannel();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(p0: Intent?): IBinder {
|
override fun onBind(p0: Intent?): IBinder {
|
||||||
|
Log.d(TAG, "in onBind")
|
||||||
return binder
|
return binder
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUnbind(intent: Intent): Boolean {
|
override fun onUnbind(intent: Intent): Boolean {
|
||||||
if (::deviceLocationManager.isInitialized) {
|
Log.d(TAG, "in UnBind")
|
||||||
deviceLocationManager.stopLocationUpdates()
|
if (!routeModel.isNavigating()) {
|
||||||
|
Log.d(TAG, "Stopping location updates")
|
||||||
|
if (::deviceLocationManager.isInitialized) {
|
||||||
|
deviceLocationManager.stopLocationUpdates()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
if (::deviceLocationManager.isInitialized) {
|
||||||
|
deviceLocationManager.stopLocationUpdates()
|
||||||
|
}
|
||||||
Log.i(TAG, "In onDestroy()");
|
Log.i(TAG, "In onDestroy()");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNotificationChannel() {
|
private fun createNotificationChannel() {
|
||||||
@@ -148,12 +177,12 @@ class NavigationService : Service() {
|
|||||||
) {
|
) {
|
||||||
Log.d(TAG, "in setCarContext")
|
Log.d(TAG, "in setCarContext")
|
||||||
this.carContext = carContext
|
this.carContext = carContext
|
||||||
|
navigationViewModel = getViewModel(carContext)
|
||||||
this.listener = listener
|
this.listener = listener
|
||||||
deviceLocationManager = DeviceLocationManagerService(
|
deviceLocationManager = DeviceLocationManagerService(
|
||||||
carContext = carContext,
|
carContext = carContext,
|
||||||
onLocationUpdate = ::updateLocation,
|
onLocationUpdate = ::updateLocation,
|
||||||
onInitialLocation = { location ->
|
onInitialLocation = { location ->
|
||||||
Log.d(TAG, "Initial location: $location")
|
|
||||||
updateLocation(location)
|
updateLocation(location)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -170,7 +199,6 @@ class NavigationService : Service() {
|
|||||||
deviceLocationManager.stopLocationUpdates()
|
deviceLocationManager.stopLocationUpdates()
|
||||||
autoDriveEnabled = true
|
autoDriveEnabled = true
|
||||||
simulation()
|
simulation()
|
||||||
//startNavigation()
|
|
||||||
CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG)
|
CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
@@ -190,9 +218,6 @@ class NavigationService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Uncomment if navigating
|
// Uncomment if navigating
|
||||||
// mNavigationManager.navigationStarted();
|
// mNavigationManager.navigationStarted();
|
||||||
}
|
}
|
||||||
@@ -205,27 +230,32 @@ class NavigationService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Starts navigation. */
|
/** Starts navigation. */
|
||||||
fun startNavigation() {
|
fun startNavigation(route: String, destination: Place) {
|
||||||
|
|
||||||
Log.i(TAG, "Starting Navigation")
|
Log.i(TAG, "Starting Navigation")
|
||||||
startService(Intent(applicationContext, NavigationService::class.java))
|
startService(Intent(applicationContext, NavigationService::class.java))
|
||||||
Log.i(TAG, "Starting foreground service")
|
routeModel.navState = routeModel.navState.copy(destination = destination)
|
||||||
|
routeModel.navState = routeModel.navState.copy(routingEngine = 2)
|
||||||
listener.navigationStateChanged(
|
routeModel.startNavigation(route)
|
||||||
isNavigating = false,
|
if (routeModel.isNavigating()) {
|
||||||
isRerouting = true,
|
routeModel.updateLocation(currentLocation, navigationViewModel)
|
||||||
hasArrived = false,
|
listener.navigationStateChanged(
|
||||||
destinations = null,
|
isNavigating = true,
|
||||||
steps = null,
|
isRerouting = false,
|
||||||
nextDestinationTravelEstimate = null,
|
hasArrived = false,
|
||||||
nextStepRemainingDistance = null,
|
destinations = mutableListOf(routeModel.getDestination()),
|
||||||
shouldShowNextStep = false,
|
destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext!!),
|
||||||
shouldShowLanes = false,
|
stepTravelEstimate = routeModel.getTravelEstimateStep(carContext!!),
|
||||||
junctionImage = null
|
steps = routeModel.getSteps(carContext!!),
|
||||||
)
|
stepRemainingDistance = routeModel.getDistance(),
|
||||||
|
shouldShowNextStep = false,
|
||||||
|
shouldShowLanes = false,
|
||||||
|
junctionImage = null,
|
||||||
|
backGroundColor = routeModel.backGroundColor()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Starts navigation. */
|
/** Starts navigation. */
|
||||||
fun stopNavigation() {
|
fun stopNavigation() {
|
||||||
if (autoDriveEnabled) {
|
if (autoDriveEnabled) {
|
||||||
@@ -234,22 +264,86 @@ class NavigationService : Service() {
|
|||||||
if (navigationManagerInitialized)
|
if (navigationManagerInitialized)
|
||||||
navigationManager.navigationEnded()
|
navigationManager.navigationEnded()
|
||||||
listener.navigationStateChanged(
|
listener.navigationStateChanged(
|
||||||
false,
|
isNavigating = false,
|
||||||
isRerouting = false,
|
isRerouting = false,
|
||||||
hasArrived = false,
|
hasArrived = false,
|
||||||
destinations = null,
|
destinations = emptyList<Destination>().toMutableList(),
|
||||||
steps = null,
|
steps = emptyList<Step>().toMutableList(),
|
||||||
nextDestinationTravelEstimate = null,
|
destinationTravelEstimate = routeModel.travelEstimate(carContext!!, 0.0, 0),
|
||||||
nextStepRemainingDistance = null,
|
stepTravelEstimate = routeModel.travelEstimate(carContext!!, 0.0, 0),
|
||||||
|
stepRemainingDistance = Distance.create(0.0, UNIT_METERS),
|
||||||
shouldShowNextStep = false,
|
shouldShowNextStep = false,
|
||||||
shouldShowLanes = false,
|
shouldShowLanes = false,
|
||||||
junctionImage = null,
|
junctionImage = null,
|
||||||
|
backGroundColor = CarColor.BLUE
|
||||||
)
|
)
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLocation(location: Location) {
|
fun updateLocation(location: Location) {
|
||||||
listener.updateServiceLocation(location)
|
Log.d(TAG, "updateLocation")
|
||||||
|
currentLocation = location
|
||||||
|
if (routeModel.isNavigating()) {
|
||||||
|
routeModel.updateLocation(location, navigationViewModel)
|
||||||
|
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
|
||||||
|
listener.updateServiceLocation(snappedLocation)
|
||||||
|
checkArrival()
|
||||||
|
updateNavigationScreen( 0)
|
||||||
|
} else {
|
||||||
|
listener.updateServiceLocation(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isNavigating(): Boolean {
|
||||||
|
return routeModel.isNavigating()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateNavigationScreen(distanceMode: Int) {
|
||||||
|
if (routeModel.isNavigating() && routeModel.navState.destination.name.isEmpty()
|
||||||
|
&& routeModel.navState.destination.street.isEmpty()
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
listener.navigationStateChanged(
|
||||||
|
isNavigating = routeModel.isNavigating(),
|
||||||
|
isRerouting = false,
|
||||||
|
hasArrived = routeModel.isArrival(),
|
||||||
|
destinations = mutableListOf(routeModel.getDestination()),
|
||||||
|
destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext!!),
|
||||||
|
stepTravelEstimate = routeModel.getTravelEstimateStep(carContext!!),
|
||||||
|
steps = routeModel.getSteps(carContext!!),
|
||||||
|
stepRemainingDistance = routeModel.getDistance(),
|
||||||
|
shouldShowNextStep = false,
|
||||||
|
shouldShowLanes = false,
|
||||||
|
junctionImage = null,
|
||||||
|
backGroundColor = routeModel.backGroundColor()
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the trip information and notifies the listener with a new Trip object.
|
||||||
|
* This includes destination name, address, travel estimate, and loading status.
|
||||||
|
*/
|
||||||
|
val tripBuilder = Trip.Builder()
|
||||||
|
tripBuilder.addDestination(
|
||||||
|
routeModel.getDestination(),
|
||||||
|
routeModel.getTravelEstimateTrip(carContext!!)
|
||||||
|
)
|
||||||
|
tripBuilder.setLoading(false)
|
||||||
|
tripBuilder.setCurrentRoad(routeModel.getDestination().name.toString())
|
||||||
|
tripBuilder.addStep(routeModel.getSteps(carContext!!).first(), routeModel.getTravelEstimateStep(carContext!!))
|
||||||
|
navigationManager.updateTrip(tripBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for arrival
|
||||||
|
*/
|
||||||
|
fun checkArrival() {
|
||||||
|
if (routeModel.isArrival()
|
||||||
|
&& routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
|
||||||
|
) {
|
||||||
|
stopNavigation()
|
||||||
|
routeModel.navState = routeModel.navState.copy(arrived = true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import androidx.car.app.model.DateTimeWithZone
|
|||||||
import androidx.car.app.model.Distance
|
import androidx.car.app.model.Distance
|
||||||
import androidx.car.app.model.DurationSpan
|
import androidx.car.app.model.DurationSpan
|
||||||
import androidx.car.app.model.ForegroundCarColorSpan
|
import androidx.car.app.model.ForegroundCarColorSpan
|
||||||
|
import androidx.car.app.navigation.model.Destination
|
||||||
import androidx.car.app.navigation.model.Lane
|
import androidx.car.app.navigation.model.Lane
|
||||||
import androidx.car.app.navigation.model.LaneDirection
|
import androidx.car.app.navigation.model.LaneDirection
|
||||||
import androidx.car.app.navigation.model.Maneuver
|
import androidx.car.app.navigation.model.Maneuver
|
||||||
@@ -99,7 +100,6 @@ class RouteCarModel : RouteModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun travelEstimate(carContext: CarContext, timeLeft: Double, distanceMode: Int): TravelEstimate {
|
fun travelEstimate(carContext: CarContext, timeLeft: Double, distanceMode: Int): TravelEstimate {
|
||||||
|
|
||||||
val timeToDestinationMillis =
|
val timeToDestinationMillis =
|
||||||
TimeUnit.SECONDS.toMillis(timeLeft.toLong())
|
TimeUnit.SECONDS.toMillis(timeLeft.toLong())
|
||||||
val distance = formattedDistance(distanceMode, routeCalculator.travelLeftDistance())
|
val distance = formattedDistance(distanceMode, routeCalculator.travelLeftDistance())
|
||||||
@@ -134,6 +134,37 @@ class RouteCarModel : RouteModel() {
|
|||||||
}
|
}
|
||||||
return travelBuilder.build()
|
return travelBuilder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSteps(carContext: CarContext): MutableList<Step> {
|
||||||
|
val steps = mutableListOf<Step>()
|
||||||
|
steps.add(currentStep(carContext))
|
||||||
|
if (navState.nextStep) {
|
||||||
|
steps.add(nextStep(carContext = carContext))
|
||||||
|
}
|
||||||
|
return steps
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDistance(): Distance {
|
||||||
|
val distance =
|
||||||
|
formattedDistance(0, routeCalculator.leftStepDistance())
|
||||||
|
return Distance.create(distance.first, distance.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTravelEstimateTrip(carContext: CarContext): TravelEstimate {
|
||||||
|
return travelEstimateTrip(carContext, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTravelEstimateStep(carContext: CarContext): TravelEstimate {
|
||||||
|
return travelEstimateStep(carContext, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDestination(): Destination {
|
||||||
|
return Destination.Builder()
|
||||||
|
.setName(navState.destination.name)
|
||||||
|
.setAddress(navState.destination.street)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
private fun createDelay(delay: Int): CarText {
|
private fun createDelay(delay: Int): CarText {
|
||||||
val delayBuilder = SpannableStringBuilder()
|
val delayBuilder = SpannableStringBuilder()
|
||||||
delayBuilder.append(
|
delayBuilder.append(
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ open class NavigationScreen(
|
|||||||
) : Screen(carContext) {
|
) : Screen(carContext) {
|
||||||
|
|
||||||
var recentPlaces = mutableListOf<Place>()
|
var recentPlaces = mutableListOf<Place>()
|
||||||
|
|
||||||
var recentPlace: Place = Place()
|
var recentPlace: Place = Place()
|
||||||
var navigationType = NavigationType.VIEW
|
var navigationType = NavigationType.VIEW
|
||||||
|
|
||||||
@@ -85,7 +84,7 @@ open class NavigationScreen(
|
|||||||
|
|
||||||
private var showAlternativeRoute = false
|
private var showAlternativeRoute = false
|
||||||
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
|
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
|
||||||
Log.d(TAG, "RecentPlaces $newPlaces")
|
Log.d(TAG, "NavigationScreen 4")
|
||||||
recentPlaces.addAll(newPlaces)
|
recentPlaces.addAll(newPlaces)
|
||||||
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
|
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
|
||||||
tripSuggestionCalled = true
|
tripSuggestionCalled = true
|
||||||
@@ -100,10 +99,11 @@ open class NavigationScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
repository.tripSuggestionFlow.asLiveData().observe(this, Observer {
|
repository.tripSuggestionFlow.asLiveData().observe(this, Observer {
|
||||||
Log.d(TAG, "tripSuggestion $it")
|
Log.d(TAG, "NavigationScreen 3")
|
||||||
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
|
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
|
||||||
tripSuggestion = it
|
tripSuggestion = it
|
||||||
})
|
})
|
||||||
|
|
||||||
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
|
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
|
||||||
showAlternativeRoute = it
|
showAlternativeRoute = it
|
||||||
})
|
})
|
||||||
@@ -119,6 +119,7 @@ open class NavigationScreen(
|
|||||||
* Returns the appropriate template based on the current navigation state.
|
* Returns the appropriate template based on the current navigation state.
|
||||||
*/
|
*/
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
|
Log.d(TAG, "NavigationScreen 2")
|
||||||
val actionStripBuilder = createActionStripBuilder({
|
val actionStripBuilder = createActionStripBuilder({
|
||||||
createAction(
|
createAction(
|
||||||
carContext,
|
carContext,
|
||||||
@@ -153,10 +154,11 @@ open class NavigationScreen(
|
|||||||
.setDestinationTravelEstimate(destinationTravelEstimate)
|
.setDestinationTravelEstimate(destinationTravelEstimate)
|
||||||
.setActionStrip(actionStripBuilder.build())
|
.setActionStrip(actionStripBuilder.build())
|
||||||
.setMapActionStrip(
|
.setMapActionStrip(
|
||||||
mapActionStrip(carContext,
|
mapActionStrip(
|
||||||
|
carContext,
|
||||||
surfaceRenderer.viewStyle,
|
surfaceRenderer.viewStyle,
|
||||||
{ zoomPlus() }, { zoomMinus() }, {
|
{ zoomPlus() }, { zoomMinus() }, {
|
||||||
Action.Builder()
|
Action.Builder()
|
||||||
.setIcon(createCarIcon(carContext, R.drawable.ic_recenter_24))
|
.setIcon(createCarIcon(carContext, R.drawable.ic_recenter_24))
|
||||||
.setFlags(0)
|
.setFlags(0)
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
@@ -174,17 +176,18 @@ open class NavigationScreen(
|
|||||||
* Creates and returns a template for the default view state.
|
* Creates and returns a template for the default view state.
|
||||||
*/
|
*/
|
||||||
private fun navigationView(actionStripBuilder: ActionStrip.Builder): Template {
|
private fun navigationView(actionStripBuilder: ActionStrip.Builder): Template {
|
||||||
val mapActionStrip = mapActionStrip(carContext,
|
val mapActionStrip = mapActionStrip(
|
||||||
surfaceRenderer.viewStyle,
|
carContext,
|
||||||
{ zoomPlus() }, { zoomMinus() }, {
|
surfaceRenderer.viewStyle,
|
||||||
createAction(
|
{ zoomPlus() }, { zoomMinus() }, {
|
||||||
carContext = carContext, R.drawable.ic_recenter_24,
|
createAction(
|
||||||
0,
|
carContext = carContext, R.drawable.ic_recenter_24,
|
||||||
onClickAction = {
|
0,
|
||||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
onClickAction = {
|
||||||
invalidate()
|
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||||
})
|
invalidate()
|
||||||
})
|
})
|
||||||
|
})
|
||||||
return NavigationTemplate.Builder()
|
return NavigationTemplate.Builder()
|
||||||
.setBackgroundColor(backGroundColor)
|
.setBackgroundColor(backGroundColor)
|
||||||
.setActionStrip(actionStripBuilder.build())
|
.setActionStrip(actionStripBuilder.build())
|
||||||
@@ -240,7 +243,8 @@ open class NavigationScreen(
|
|||||||
.setBackgroundColor(backGroundColor)
|
.setBackgroundColor(backGroundColor)
|
||||||
.setActionStrip(actionStripBuilder.build())
|
.setActionStrip(actionStripBuilder.build())
|
||||||
.setMapActionStrip(
|
.setMapActionStrip(
|
||||||
mapActionStrip(carContext,
|
mapActionStrip(
|
||||||
|
carContext,
|
||||||
surfaceRenderer.viewStyle,
|
surfaceRenderer.viewStyle,
|
||||||
{ zoomPlus() }, { zoomMinus() }, {
|
{ zoomPlus() }, { zoomMinus() }, {
|
||||||
createAction(
|
createAction(
|
||||||
@@ -301,7 +305,8 @@ open class NavigationScreen(
|
|||||||
val builder = MapWithContentTemplate.Builder()
|
val builder = MapWithContentTemplate.Builder()
|
||||||
.setContentTemplate(contentTemplate)
|
.setContentTemplate(contentTemplate)
|
||||||
.setActionStrip(
|
.setActionStrip(
|
||||||
mapActionStrip(carContext,
|
mapActionStrip(
|
||||||
|
carContext,
|
||||||
ViewStyle.VIEW,
|
ViewStyle.VIEW,
|
||||||
{ settingsAction() },
|
{ settingsAction() },
|
||||||
{
|
{
|
||||||
@@ -340,10 +345,12 @@ open class NavigationScreen(
|
|||||||
*/
|
*/
|
||||||
fun getRoutingInfo(): RoutingInfo {
|
fun getRoutingInfo(): RoutingInfo {
|
||||||
val routingInfo = RoutingInfo.Builder()
|
val routingInfo = RoutingInfo.Builder()
|
||||||
.setCurrentStep(
|
if (steps.isNotEmpty()) {
|
||||||
|
routingInfo.setCurrentStep(
|
||||||
steps.first(),
|
steps.first(),
|
||||||
stepRemainingDistance
|
stepRemainingDistance
|
||||||
)
|
)
|
||||||
|
}
|
||||||
if (shouldShowNextStep && steps.size > 1) {
|
if (shouldShowNextStep && steps.size > 1) {
|
||||||
routingInfo.setNextStep(steps[1])
|
routingInfo.setNextStep(steps[1])
|
||||||
}
|
}
|
||||||
@@ -491,7 +498,6 @@ open class NavigationScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates navigation state with the current location, checks for arrival, and traffic updates.
|
* Updates navigation state with the current location, checks for arrival, and traffic updates.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,6 @@
|
|||||||
package com.kouros.navigation.car.screen.observers
|
package com.kouros.navigation.car.screen.observers
|
||||||
|
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.data.nominatim.SearchResult
|
|
||||||
import com.kouros.navigation.data.overpass.Elements
|
import com.kouros.navigation.data.overpass.Elements
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,6 +25,8 @@ interface NavigationObserverCallback {
|
|||||||
/** Called when max speed is updated */
|
/** Called when max speed is updated */
|
||||||
fun onMaxSpeedReceived(speed: Int)
|
fun onMaxSpeedReceived(speed: Int)
|
||||||
|
|
||||||
|
fun onRecentPlacesReceived(places: List<Place>)
|
||||||
|
|
||||||
/** Called to request UI invalidation/refresh */
|
/** Called to request UI invalidation/refresh */
|
||||||
fun invalidateScreen()
|
fun invalidateScreen()
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -1,5 +1,6 @@
|
|||||||
package com.kouros.navigation.car.screen.observers
|
package com.kouros.navigation.car.screen.observers
|
||||||
|
|
||||||
|
import com.kouros.navigation.car.CarSession
|
||||||
import com.kouros.navigation.car.NavigationSession
|
import com.kouros.navigation.car.NavigationSession
|
||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
|
|
||||||
@@ -18,12 +19,18 @@ class NavigationObserverManager(
|
|||||||
val speedCameraObserver = SpeedCameraObserver(callback)
|
val speedCameraObserver = SpeedCameraObserver(callback)
|
||||||
val maxSpeedObserver = MaxSpeedObserver(callback)
|
val maxSpeedObserver = MaxSpeedObserver(callback)
|
||||||
|
|
||||||
fun attachAllObservers(session: NavigationSession) {
|
val recentPlacesObserver = RecentPlacesObserver(callback)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fun attachAllObservers(session: CarSession) {
|
||||||
viewModel.route.observe(session, routeObserver)
|
viewModel.route.observe(session, routeObserver)
|
||||||
viewModel.traffic.observe(session, trafficObserver)
|
viewModel.traffic.observe(session, trafficObserver)
|
||||||
viewModel.placeLocation.observe(session, placeSearchObserver)
|
viewModel.placeLocation.observe(session, placeSearchObserver)
|
||||||
viewModel.speedCameras.observe(session, speedCameraObserver)
|
viewModel.speedCameras.observe(session, speedCameraObserver)
|
||||||
viewModel.maxSpeed.observe(session, maxSpeedObserver)
|
viewModel.maxSpeed.observe(session, maxSpeedObserver)
|
||||||
|
viewModel.recentPlaces.observe(session, recentPlacesObserver)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.kouros.navigation.car.screen.observers
|
||||||
|
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import com.kouros.navigation.data.Place
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observer for route updates. Triggers navigation start when a non-empty route is received.
|
||||||
|
*/
|
||||||
|
class RecentPlacesObserver(
|
||||||
|
private val callback: NavigationObserverCallback
|
||||||
|
) : Observer<List<Place>> {
|
||||||
|
|
||||||
|
override fun onChanged(value: List<Place>) {
|
||||||
|
if (value.isNotEmpty()) {
|
||||||
|
callback.onRecentPlacesReceived(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.kouros.navigation.model
|
|||||||
//import com.kouros.navigation.data.Preferences.boxStore
|
//import com.kouros.navigation.data.Preferences.boxStore
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import androidx.compose.runtime.toMutableStateList
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
@@ -10,6 +11,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.kouros.navigation.data.Constants
|
import com.kouros.navigation.data.Constants
|
||||||
|
import com.kouros.navigation.data.Constants.TAG
|
||||||
import com.kouros.navigation.data.NavigationRepository
|
import com.kouros.navigation.data.NavigationRepository
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.data.Places
|
import com.kouros.navigation.data.Places
|
||||||
|
|||||||
Reference in New Issue
Block a user