Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ce079a7c1 | ||
|
|
8af2d3ad0b | ||
|
|
1d67b3cc06 | ||
|
|
a4227c80d3 | ||
|
|
757c4c8d8d |
@@ -45,6 +45,12 @@
|
|||||||
android:foregroundServiceType="location"
|
android:foregroundServiceType="location"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
</service>
|
</service>
|
||||||
|
<service
|
||||||
|
android:name=".car.navigation.NavigationService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:foregroundServiceType="location"
|
||||||
|
android:exported="true">
|
||||||
|
</service>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.kouros.navigation.car
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.location.Location
|
||||||
|
import android.location.LocationManager
|
||||||
|
import androidx.car.app.CarContext
|
||||||
|
import androidx.core.location.LocationListenerCompat
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.kouros.navigation.car.navigation.NavigationService
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages device GPS location updates for navigation.
|
||||||
|
* Coordinates with car hardware sensors to avoid duplicate location sources.
|
||||||
|
*
|
||||||
|
* @param carContext The car context for accessing system services
|
||||||
|
* @param serviceOwner Owner of the lifecycle for coroutine management
|
||||||
|
* @param shouldUseCarLocationFlow Flow indicating whether car location hardware should be used
|
||||||
|
* @param onLocationUpdate Callback invoked when location updates are received
|
||||||
|
* @param onInitialLocation Callback invoked with the last known location when starting
|
||||||
|
*/
|
||||||
|
class DeviceLocationManagerService(
|
||||||
|
private val carContext: CarContext,
|
||||||
|
private val onLocationUpdate: (Location) -> Unit,
|
||||||
|
private val onInitialLocation: (Location) -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val locationManager: LocationManager =
|
||||||
|
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||||
|
|
||||||
|
private var shouldUseDeviceLocation = true
|
||||||
|
private var isListening = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Location listener that receives GPS updates from the device.
|
||||||
|
* Only processes location if car location hardware is not being used.
|
||||||
|
*/
|
||||||
|
private val locationListener: LocationListenerCompat = LocationListenerCompat { location ->
|
||||||
|
if (shouldUseDeviceLocation) {
|
||||||
|
onLocationUpdate(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts requesting location updates from device GPS.
|
||||||
|
* Provides initial location via callback and then starts continuous updates.
|
||||||
|
*
|
||||||
|
* @param minTimeMs Minimum time interval between updates in milliseconds (default: 500ms)
|
||||||
|
* @param minDistanceM Minimum distance between updates in meters (default: 5m)
|
||||||
|
*/
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 1.0f) {
|
||||||
|
if (isListening) return
|
||||||
|
|
||||||
|
// Get and deliver last known location first
|
||||||
|
val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
|
||||||
|
if (lastLocation != null) {
|
||||||
|
onInitialLocation(lastLocation)
|
||||||
|
onLocationUpdate(lastLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start continuous location updates
|
||||||
|
locationManager.requestLocationUpdates(
|
||||||
|
LocationManager.GPS_PROVIDER,
|
||||||
|
minTimeMs,
|
||||||
|
minDistanceM,
|
||||||
|
locationListener
|
||||||
|
)
|
||||||
|
isListening = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops receiving location updates from device GPS.
|
||||||
|
* Should be called when the session is destroyed to prevent memory leaks.
|
||||||
|
*/
|
||||||
|
fun stopLocationUpdates() {
|
||||||
|
if (!isListening) return
|
||||||
|
|
||||||
|
locationManager.removeUpdates(locationListener)
|
||||||
|
isListening = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if location updates are currently active.
|
||||||
|
*/
|
||||||
|
fun isListeningForUpdates(): Boolean = isListening
|
||||||
|
}
|
||||||
@@ -1,22 +1,28 @@
|
|||||||
package com.kouros.navigation.car
|
package com.kouros.navigation.car
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
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.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
|
||||||
@@ -27,6 +33,7 @@ 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
|
||||||
@@ -68,7 +75,6 @@ import kotlinx.coroutines.launch
|
|||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import kotlin.collections.listOf
|
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
|
||||||
@@ -98,9 +104,12 @@ 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
|
||||||
|
|
||||||
lateinit var navigationManager: NavigationManager
|
var initialLocation = true;
|
||||||
|
|
||||||
|
// lateinit var navigationManager: NavigationManager
|
||||||
|
|
||||||
lateinit var textToSpeechManager: TextToSpeechManager
|
lateinit var textToSpeechManager: TextToSpeechManager
|
||||||
|
|
||||||
@@ -126,12 +135,73 @@ 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)
|
||||||
@@ -142,10 +212,16 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
|||||||
super.onResume(owner)
|
super.onResume(owner)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy(owner: LifecycleOwner) {
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
if (::navigationManager.isInitialized) {
|
Log.i(TAG, "In onStop()")
|
||||||
navigationManager.clearNavigationManagerCallback()
|
carContext.unbindService(serviceConnection)
|
||||||
|
navigationService = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
// if (::navigationManager.isInitialized) {
|
||||||
|
// navigationManager.clearNavigationManagerCallback()
|
||||||
|
// }
|
||||||
if (::carSensorManager.isInitialized) {
|
if (::carSensorManager.isInitialized) {
|
||||||
carSensorManager.cleanup()
|
carSensorManager.cleanup()
|
||||||
}
|
}
|
||||||
@@ -192,6 +268,7 @@ 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())
|
||||||
@@ -302,26 +379,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(
|
||||||
@@ -332,6 +409,7 @@ 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,
|
||||||
@@ -345,6 +423,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
textToSpeechManager = TextToSpeechManager(carContext)
|
textToSpeechManager = TextToSpeechManager(carContext)
|
||||||
@@ -378,6 +457,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 {
|
||||||
@@ -452,6 +532,7 @@ 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 {
|
||||||
@@ -600,10 +681,13 @@ 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(
|
||||||
@@ -622,9 +706,12 @@ 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
|
||||||
@@ -635,11 +722,12 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -372,6 +372,7 @@ class SurfaceRenderer(
|
|||||||
* Uses car orientation sensor if available, otherwise falls back to location bearing.
|
* Uses car orientation sensor if available, otherwise falls back to location bearing.
|
||||||
*/
|
*/
|
||||||
fun updateLocation(location: Location, streetName: String) {
|
fun updateLocation(location: Location, streetName: String) {
|
||||||
|
Log.d(TAG, "updateLocation Surface $location $streetName")
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
street.value = streetName
|
street.value = streetName
|
||||||
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
|
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
package com.kouros.navigation.car.navigation
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.location.Location
|
||||||
|
import android.location.LocationManager
|
||||||
|
import android.os.Binder
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresPermission
|
||||||
|
import androidx.car.app.CarContext
|
||||||
|
import androidx.car.app.CarToast
|
||||||
|
import androidx.car.app.model.CarIcon
|
||||||
|
import androidx.car.app.model.Distance
|
||||||
|
import androidx.car.app.navigation.NavigationManager
|
||||||
|
import androidx.car.app.navigation.NavigationManagerCallback
|
||||||
|
import androidx.car.app.navigation.model.Destination
|
||||||
|
import androidx.car.app.navigation.model.Step
|
||||||
|
import androidx.car.app.navigation.model.TravelEstimate
|
||||||
|
import com.kouros.navigation.car.DeviceLocationManagerService
|
||||||
|
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||||
|
import io.ticofab.androidgpxparser.parser.GPXParser
|
||||||
|
import io.ticofab.androidgpxparser.parser.domain.Gpx
|
||||||
|
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
|
|
||||||
|
class NavigationService : Service() {
|
||||||
|
|
||||||
|
val TAG: String = "NavigationService"
|
||||||
|
|
||||||
|
val DEEP_LINK_ACTION: String = ("com.kouros.navigation.car.navigation"
|
||||||
|
+ ".NavigationDeepLinkAction")
|
||||||
|
|
||||||
|
val channelId: String = "NavigationServiceChannel"
|
||||||
|
|
||||||
|
/** The identifier for the navigation notification displayed for the foreground service. */
|
||||||
|
|
||||||
|
val NAV_NOTIFICATION_ID: Int = 87356325
|
||||||
|
|
||||||
|
/** The identifier for the non-navigation notifications, such as a traffic accident warning. */
|
||||||
|
|
||||||
|
val NOTIFICATION_ID: Int = 71653346
|
||||||
|
|
||||||
|
// Constants for location broadcast
|
||||||
|
val PACKAGE_NAME: String =
|
||||||
|
"androidx.car.app.sample.navigation.common.nav.navigationservice"
|
||||||
|
|
||||||
|
val EXTRA_STARTED_FROM_NOTIFICATION: String = PACKAGE_NAME + ".started_from_notification"
|
||||||
|
|
||||||
|
val CANCEL_ACTION: String = "CANCEL"
|
||||||
|
|
||||||
|
private var notificationManager: NotificationManager? = null
|
||||||
|
private var carContext: CarContext? = null
|
||||||
|
|
||||||
|
var autoDriveEnabled = false
|
||||||
|
|
||||||
|
val simulation = Simulation()
|
||||||
|
|
||||||
|
private lateinit var listener: Listener
|
||||||
|
|
||||||
|
// Model for managing route state and navigation logic for Android Auto
|
||||||
|
var routeModel = RouteCarModel()
|
||||||
|
|
||||||
|
// Manages device GPS location updates
|
||||||
|
lateinit var deviceLocationManager: DeviceLocationManagerService
|
||||||
|
|
||||||
|
private lateinit var navigationManager: NavigationManager
|
||||||
|
private var navigationManagerInitialized = false
|
||||||
|
var binder: IBinder = LocalBinder()
|
||||||
|
|
||||||
|
|
||||||
|
/** A listener for the navigation state changes. */
|
||||||
|
interface Listener {
|
||||||
|
/** Callback called when the navigation state changes. */
|
||||||
|
fun navigationStateChanged(
|
||||||
|
isNavigating: Boolean,
|
||||||
|
isRerouting: Boolean,
|
||||||
|
hasArrived: Boolean,
|
||||||
|
destinations: MutableList<Destination?>?,
|
||||||
|
steps: MutableList<Step>?,
|
||||||
|
nextDestinationTravelEstimate: TravelEstimate?,
|
||||||
|
nextStepRemainingDistance: Distance?,
|
||||||
|
shouldShowNextStep: Boolean,
|
||||||
|
shouldShowLanes: Boolean,
|
||||||
|
junctionImage: CarIcon?
|
||||||
|
)
|
||||||
|
|
||||||
|
fun updateServiceLocation(location: Location)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used for the client Binder. Since this service runs in the same process as its clients,
|
||||||
|
* we don't need to deal with IPC.
|
||||||
|
*/
|
||||||
|
inner class LocalBinder : Binder() {
|
||||||
|
val service: NavigationService
|
||||||
|
get() = this@NavigationService
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
Log.i(TAG, "In onCreate()");
|
||||||
|
createNotificationChannel();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(p0: Intent?): IBinder {
|
||||||
|
return binder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnbind(intent: Intent): Boolean {
|
||||||
|
if (::deviceLocationManager.isInitialized) {
|
||||||
|
deviceLocationManager.stopLocationUpdates()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
Log.i(TAG, "In onDestroy()");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotificationChannel() {
|
||||||
|
val serviceChannel = NotificationChannel(
|
||||||
|
"CHANNEL_ID",
|
||||||
|
"Location Service Channel",
|
||||||
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
|
)
|
||||||
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
|
manager.createNotificationChannel(serviceChannel)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Sets the [CarContext] to use while the service is connected. */
|
||||||
|
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||||
|
fun setCarContext(
|
||||||
|
carContext: CarContext,
|
||||||
|
listener: Listener
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "in setCarContext")
|
||||||
|
this.carContext = carContext
|
||||||
|
this.listener = listener
|
||||||
|
deviceLocationManager = DeviceLocationManagerService(
|
||||||
|
carContext = carContext,
|
||||||
|
onLocationUpdate = ::updateLocation,
|
||||||
|
onInitialLocation = { location ->
|
||||||
|
Log.d(TAG, "Initial location: $location")
|
||||||
|
updateLocation(location)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
deviceLocationManager.startLocationUpdates()
|
||||||
|
|
||||||
|
navigationManagerInitialized = true
|
||||||
|
navigationManager =
|
||||||
|
carContext.getCarService(NavigationManager::class.java)
|
||||||
|
navigationManager.setNavigationManagerCallback(object : NavigationManagerCallback {
|
||||||
|
override fun onAutoDriveEnabled() {
|
||||||
|
Log.d(TAG, "onAutoDriveEnabled")
|
||||||
|
// Called when the app should simulate navigation (e.g., for testing)
|
||||||
|
deviceLocationManager.stopLocationUpdates()
|
||||||
|
autoDriveEnabled = true
|
||||||
|
simulation()
|
||||||
|
//startNavigation()
|
||||||
|
CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun simulation() {
|
||||||
|
simulation.gpxSimulation {
|
||||||
|
listener.updateServiceLocation(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopNavigation() {
|
||||||
|
// Called when the user stops navigation in the car screen
|
||||||
|
// Stop turn-by-turn logic and clean up
|
||||||
|
stopNavigation()
|
||||||
|
if (autoDriveEnabled) {
|
||||||
|
deviceLocationManager.startLocationUpdates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Uncomment if navigating
|
||||||
|
// mNavigationManager.navigationStarted();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clears the currently used {@link CarContext}. */
|
||||||
|
fun clearCarContext() {
|
||||||
|
Log.i(TAG, "clearContext");
|
||||||
|
carContext = null;
|
||||||
|
navigationManager.clearNavigationManagerCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Starts navigation. */
|
||||||
|
fun startNavigation() {
|
||||||
|
|
||||||
|
Log.i(TAG, "Starting Navigation")
|
||||||
|
startService(Intent(applicationContext, NavigationService::class.java))
|
||||||
|
Log.i(TAG, "Starting foreground service")
|
||||||
|
|
||||||
|
listener.navigationStateChanged(
|
||||||
|
isNavigating = false,
|
||||||
|
isRerouting = true,
|
||||||
|
hasArrived = false,
|
||||||
|
destinations = null,
|
||||||
|
steps = null,
|
||||||
|
nextDestinationTravelEstimate = null,
|
||||||
|
nextStepRemainingDistance = null,
|
||||||
|
shouldShowNextStep = false,
|
||||||
|
shouldShowLanes = false,
|
||||||
|
junctionImage = null
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Starts navigation. */
|
||||||
|
fun stopNavigation() {
|
||||||
|
if (autoDriveEnabled) {
|
||||||
|
autoDriveEnabled = false
|
||||||
|
}
|
||||||
|
if (navigationManagerInitialized)
|
||||||
|
navigationManager.navigationEnded()
|
||||||
|
listener.navigationStateChanged(
|
||||||
|
false,
|
||||||
|
isRerouting = false,
|
||||||
|
hasArrived = false,
|
||||||
|
destinations = null,
|
||||||
|
steps = null,
|
||||||
|
nextDestinationTravelEstimate = null,
|
||||||
|
nextStepRemainingDistance = null,
|
||||||
|
shouldShowNextStep = false,
|
||||||
|
shouldShowLanes = false,
|
||||||
|
junctionImage = null,
|
||||||
|
)
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateLocation(location: Location) {
|
||||||
|
listener.updateServiceLocation(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,10 @@ package com.kouros.navigation.car.navigation
|
|||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import com.kouros.data.BuildConfig
|
import com.kouros.data.BuildConfig
|
||||||
|
import com.kouros.navigation.data.Constants.TAG
|
||||||
import com.kouros.navigation.data.tomtom.TomTomRepository
|
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||||
import io.ticofab.androidgpxparser.parser.GPXParser
|
import io.ticofab.androidgpxparser.parser.GPXParser
|
||||||
import io.ticofab.androidgpxparser.parser.domain.Gpx
|
import io.ticofab.androidgpxparser.parser.domain.Gpx
|
||||||
@@ -27,8 +29,9 @@ class Simulation {
|
|||||||
) {
|
) {
|
||||||
if (routeModel.navState.route.isRouteValid()) {
|
if (routeModel.navState.route.isRouteValid()) {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
//gpxSimulation(routeModel, lifecycleScope, updateLocation)
|
gpxSimulation(routeModel, lifecycleScope, updateLocation)
|
||||||
currentSimulation(routeModel, lifecycleScope, updateLocation)
|
//gpxSimulation(updateLocation)
|
||||||
|
//currentSimulation(routeModel, lifecycleScope, updateLocation)
|
||||||
} else {
|
} else {
|
||||||
currentSimulation(routeModel, lifecycleScope, updateLocation)
|
currentSimulation(routeModel, lifecycleScope, updateLocation)
|
||||||
}
|
}
|
||||||
@@ -134,4 +137,62 @@ class Simulation {
|
|||||||
fun stopSimulation() {
|
fun stopSimulation() {
|
||||||
simulationJob?.cancel()
|
simulationJob?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun gpxSimulation(
|
||||||
|
updateLocation: (Location) -> Unit
|
||||||
|
) {
|
||||||
|
Runnable {
|
||||||
|
var route = ""
|
||||||
|
simulationJob?.cancel()
|
||||||
|
runBlocking {
|
||||||
|
simulationJob = launch(Dispatchers.IO) {
|
||||||
|
route = TomTomRepository().fetchUrl(
|
||||||
|
"https://kouros-online.de/vh.gpx",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
simulationJob?.join()
|
||||||
|
}
|
||||||
|
simulationJob?.cancel()
|
||||||
|
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
|
||||||
|
var curBearing = 0f
|
||||||
|
val parser = GPXParser()
|
||||||
|
val parsedGpx: Gpx? =
|
||||||
|
parser.parse(route.byteInputStream())
|
||||||
|
parsedGpx?.let {
|
||||||
|
val tracks = parsedGpx.tracks
|
||||||
|
tracks.forEach { tr ->
|
||||||
|
val segments: MutableList<TrackSegment?>? = tr.trackSegments
|
||||||
|
segments!!.forEach { seg ->
|
||||||
|
var lastTime = DateTime.now()
|
||||||
|
seg!!.trackPoints.forEach { p ->
|
||||||
|
val ext = p.extensions
|
||||||
|
var curSpeed = 0F
|
||||||
|
if (ext != null) {
|
||||||
|
curSpeed = ext.speed.toFloat()
|
||||||
|
}
|
||||||
|
val duration = p.time.millis - lastTime.millis
|
||||||
|
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
|
||||||
|
latitude = p.latitude
|
||||||
|
longitude = p.longitude
|
||||||
|
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
|
||||||
|
speed = curSpeed
|
||||||
|
time = System.currentTimeMillis()
|
||||||
|
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
|
||||||
|
}
|
||||||
|
// Update your app's state as if a real GPS update occurred
|
||||||
|
updateLocation(fakeLocation)
|
||||||
|
// Wait before moving to the next point (e.g., every 1 second)
|
||||||
|
if (duration > 100) {
|
||||||
|
// delay(duration / 4)
|
||||||
|
}
|
||||||
|
Thread.sleep(2000)
|
||||||
|
lastTime = p.time
|
||||||
|
lastLocation = fakeLocation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.run()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -85,6 +85,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")
|
||||||
recentPlaces.addAll(newPlaces)
|
recentPlaces.addAll(newPlaces)
|
||||||
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
|
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
|
||||||
tripSuggestionCalled = true
|
tripSuggestionCalled = true
|
||||||
@@ -99,6 +100,7 @@ open class NavigationScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
repository.tripSuggestionFlow.asLiveData().observe(this, Observer {
|
repository.tripSuggestionFlow.asLiveData().observe(this, Observer {
|
||||||
|
Log.d(TAG, "tripSuggestion $it")
|
||||||
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
|
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
|
||||||
tripSuggestion = it
|
tripSuggestion = it
|
||||||
})
|
})
|
||||||
@@ -519,7 +521,6 @@ open class NavigationScreen(
|
|||||||
this.shouldShowLanes = shouldShowLanes
|
this.shouldShowLanes = shouldShowLanes
|
||||||
this.junctionImage = junctionImage
|
this.junctionImage = junctionImage
|
||||||
this.backGroundColor = backGroundColor
|
this.backGroundColor = backGroundColor
|
||||||
|
|
||||||
navigationType = NavigationType.NAVIGATION
|
navigationType = NavigationType.NAVIGATION
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user