Compare commits

..
5 Commits
Author SHA1 Message Date
Dimitris 2ce079a7c1 Service 2026-04-03 12:30:39 +02:00
Dimitris 8af2d3ad0b Service 2026-04-03 10:00:00 +02:00
Dimitris 1d67b3cc06 Arrival Issue 2026-04-03 09:59:32 +02:00
Dimitris a4227c80d3 NavigationService 2026-04-03 09:57:57 +02:00
Dimitris 757c4c8d8d NavigationService 2026-04-03 09:57:52 +02:00
7 changed files with 558 additions and 49 deletions
+6
View File
@@ -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()
} }