Claude refactoring
This commit is contained in:
@@ -14,8 +14,8 @@ android {
|
|||||||
applicationId = "com.kouros.navigation"
|
applicationId = "com.kouros.navigation"
|
||||||
minSdk = 33
|
minSdk = 33
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 49
|
versionCode = 50
|
||||||
versionName = "0.2.0.49"
|
versionName = "0.2.0.50"
|
||||||
base.archivesName = "navi-$versionName"
|
base.archivesName = "navi-$versionName"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.content.Context
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -22,45 +22,83 @@ import com.kouros.data.R
|
|||||||
import com.kouros.navigation.data.StepData
|
import com.kouros.navigation.data.StepData
|
||||||
import com.kouros.navigation.utils.round
|
import com.kouros.navigation.utils.round
|
||||||
|
|
||||||
|
private const val MANEUVER_TYPE_EXIT_RIGHT = 45
|
||||||
|
private const val MANEUVER_TYPE_EXIT_LEFT = 46
|
||||||
|
private const val METERS_PER_KILOMETER = 1000.0
|
||||||
|
private const val DISTANCE_THRESHOLD_METERS = 1000
|
||||||
|
|
||||||
|
private val CardTopPadding = 60.dp
|
||||||
|
private val CardElevation = 6.dp
|
||||||
|
private val IconSize = 48.dp
|
||||||
|
private val ExitTextSize = 18.sp
|
||||||
|
private val PrimaryTextSize = 24.sp
|
||||||
|
private val SpacerWidth = 8.dp
|
||||||
|
private val CardPadding = 16.dp
|
||||||
|
private val ElementSpacing = 8.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigationInfo(step: StepData?, nextStep: StepData?) {
|
fun NavigationInfo(
|
||||||
if (step != null && step.instruction.isNotEmpty()) {
|
step: StepData?,
|
||||||
|
nextStep: StepData?
|
||||||
|
) {
|
||||||
|
step?.takeIf { it.instruction.isNotEmpty() }?.let { currentStep ->
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
elevation = CardDefaults.cardElevation(
|
elevation = CardDefaults.cardElevation(defaultElevation = CardElevation),
|
||||||
defaultElevation = 6.dp
|
modifier = Modifier
|
||||||
|
.padding(top = CardTopPadding)
|
||||||
), modifier = Modifier
|
|
||||||
.padding(top = 60.dp)
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Column {
|
Column(
|
||||||
Icon(
|
modifier = Modifier.padding(CardPadding),
|
||||||
painter = painterResource(step.icon),
|
horizontalAlignment = Alignment.Start
|
||||||
contentDescription = stringResource(id = R.string.accept_action_title),
|
|
||||||
modifier = Modifier.size(48.dp, 48.dp),
|
|
||||||
)
|
|
||||||
if (step.currentManeuverType == 46
|
|
||||||
|| step.currentManeuverType == 45
|
|
||||||
) {
|
) {
|
||||||
Text(text = "Exit ${step.exitNumber}", fontSize = 18.sp)
|
Icon(
|
||||||
}
|
painter = painterResource(currentStep.icon),
|
||||||
Row {
|
contentDescription = stringResource(id = R.string.navigation_icon_description),
|
||||||
if (step.leftStepDistance < 1000) {
|
modifier = Modifier.size(IconSize),
|
||||||
Text(text = "${step.leftStepDistance.toInt()} m", fontSize = 24.sp, color = MaterialTheme.colorScheme.primary)
|
tint = MaterialTheme.colorScheme.primary
|
||||||
} else {
|
)
|
||||||
|
|
||||||
|
if (currentStep.isExitManeuver) {
|
||||||
Text(
|
Text(
|
||||||
text = "${(step.leftStepDistance / 1000).round(1)} km",
|
text = stringResource(R.string.exit_number, currentStep.exitNumber),
|
||||||
fontSize = 24.sp,
|
fontSize = ExitTextSize,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.padding(top = ElementSpacing)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(top = ElementSpacing)
|
||||||
|
) {
|
||||||
|
DistanceText(distance = currentStep.leftStepDistance)
|
||||||
|
Spacer(modifier = Modifier.padding(horizontal = SpacerWidth))
|
||||||
|
Text(
|
||||||
|
text = currentStep.instruction,
|
||||||
|
fontSize = PrimaryTextSize,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(
|
}
|
||||||
modifier = Modifier.padding(5.dp)
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DistanceText(distance: Double) {
|
||||||
|
val formattedDistance = when {
|
||||||
|
distance < DISTANCE_THRESHOLD_METERS -> "${distance.toInt()} m"
|
||||||
|
else -> "${(distance / METERS_PER_KILOMETER).round(1)} km"
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = formattedDistance,
|
||||||
|
fontSize = PrimaryTextSize,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
Text(text = step.instruction, fontSize = 24.sp, color = MaterialTheme.colorScheme.primary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val StepData.isExitManeuver: Boolean
|
||||||
|
get() = currentManeuverType == MANEUVER_TYPE_EXIT_RIGHT ||
|
||||||
|
currentManeuverType == MANEUVER_TYPE_EXIT_LEFT
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package com.kouros.navigation.car
|
||||||
|
|
||||||
|
import android.location.Location
|
||||||
|
import androidx.car.app.CarContext
|
||||||
|
import androidx.car.app.connection.CarConnection
|
||||||
|
import androidx.car.app.hardware.CarHardwareManager
|
||||||
|
import androidx.car.app.hardware.common.CarValue
|
||||||
|
import androidx.car.app.hardware.common.OnCarDataAvailableListener
|
||||||
|
import androidx.car.app.hardware.info.CarHardwareLocation
|
||||||
|
import androidx.car.app.hardware.info.CarSensors
|
||||||
|
import androidx.car.app.hardware.info.Compass
|
||||||
|
import androidx.car.app.hardware.info.Speed
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.kouros.navigation.utils.getSettingsRepository
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages car hardware sensor listeners for navigation.
|
||||||
|
* Handles location, compass, and speed sensors from the car hardware.
|
||||||
|
*
|
||||||
|
* @param carContext The car context for accessing hardware services
|
||||||
|
* @param lifecycleOwner Owner of the lifecycle for coroutine management
|
||||||
|
* @param onLocationUpdate Callback for location updates
|
||||||
|
* @param onCompassUpdate Callback for compass/orientation updates
|
||||||
|
* @param onSpeedUpdate Callback for speed updates
|
||||||
|
*/
|
||||||
|
class CarSensorManager(
|
||||||
|
private val carContext: CarContext,
|
||||||
|
private val lifecycleOwner: LifecycleOwner,
|
||||||
|
private val onLocationUpdate: (Location) -> Unit,
|
||||||
|
private val onCompassUpdate: (Float) -> Unit,
|
||||||
|
private val onSpeedUpdate: (Float) -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val carHardwareManager: CarHardwareManager =
|
||||||
|
carContext.getCarService(CarHardwareManager::class.java)
|
||||||
|
|
||||||
|
private val settingsRepository = getSettingsRepository(carContext)
|
||||||
|
|
||||||
|
private var carConnection: Int = CarConnection.CONNECTION_TYPE_NOT_CONNECTED
|
||||||
|
private var isLocationSensorActive = false
|
||||||
|
private var isSpeedSensorActive = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Car hardware location listener.
|
||||||
|
* Receives location data from the car's GPS system.
|
||||||
|
*/
|
||||||
|
private val carLocationListener: OnCarDataAvailableListener<CarHardwareLocation?> =
|
||||||
|
OnCarDataAvailableListener { data ->
|
||||||
|
if (data.location.status == CarValue.STATUS_SUCCESS) {
|
||||||
|
val location = data.location.value
|
||||||
|
if (location != null) {
|
||||||
|
onLocationUpdate(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Car compass/orientation sensor listener.
|
||||||
|
* Updates orientation for map rotation.
|
||||||
|
*/
|
||||||
|
private val carCompassListener: OnCarDataAvailableListener<Compass?> =
|
||||||
|
OnCarDataAvailableListener { data ->
|
||||||
|
if (data.orientations.status == CarValue.STATUS_SUCCESS) {
|
||||||
|
val orientation = data.orientations.value
|
||||||
|
if (!orientation.isNullOrEmpty()) {
|
||||||
|
onCompassUpdate(orientation[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Car speed sensor listener.
|
||||||
|
* Receives speed in meters per second from car hardware.
|
||||||
|
*/
|
||||||
|
private val carSpeedListener = OnCarDataAvailableListener<Speed> { data ->
|
||||||
|
if (data.displaySpeedMetersPerSecond.status == CarValue.STATUS_SUCCESS) {
|
||||||
|
val speed = data.displaySpeedMetersPerSecond.value
|
||||||
|
if (speed != null) {
|
||||||
|
onSpeedUpdate(speed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Observe car location setting changes
|
||||||
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
|
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
settingsRepository.carLocationFlow.collectLatest { useCarLocation ->
|
||||||
|
if (useCarLocation) {
|
||||||
|
addLocationSensors()
|
||||||
|
} else {
|
||||||
|
removeLocationSensors()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the car connection state and manages speed sensor accordingly.
|
||||||
|
*
|
||||||
|
* @param connectionState The current car connection type
|
||||||
|
*/
|
||||||
|
fun updateConnectionState(connectionState: Int) {
|
||||||
|
carConnection = connectionState
|
||||||
|
when (connectionState) {
|
||||||
|
CarConnection.CONNECTION_TYPE_NATIVE,
|
||||||
|
CarConnection.CONNECTION_TYPE_PROJECTION -> addSpeedSensor()
|
||||||
|
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> removeSpeedSensor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if car location sensors should be used based on settings.
|
||||||
|
*
|
||||||
|
* @return Flow of boolean indicating if car location should be used
|
||||||
|
*/
|
||||||
|
fun shouldUseCarLocation() = settingsRepository.carLocationFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds location and compass sensors if not already active.
|
||||||
|
*/
|
||||||
|
private fun addLocationSensors() {
|
||||||
|
if (isLocationSensorActive) return
|
||||||
|
|
||||||
|
val carSensors = carHardwareManager.carSensors
|
||||||
|
carSensors.addCompassListener(
|
||||||
|
CarSensors.UPDATE_RATE_NORMAL,
|
||||||
|
carContext.mainExecutor,
|
||||||
|
carCompassListener
|
||||||
|
)
|
||||||
|
carSensors.addCarHardwareLocationListener(
|
||||||
|
CarSensors.UPDATE_RATE_FASTEST,
|
||||||
|
carContext.mainExecutor,
|
||||||
|
carLocationListener
|
||||||
|
)
|
||||||
|
isLocationSensorActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes location and compass sensors.
|
||||||
|
*/
|
||||||
|
private fun removeLocationSensors() {
|
||||||
|
if (!isLocationSensorActive) return
|
||||||
|
|
||||||
|
val carSensors = carHardwareManager.carSensors
|
||||||
|
carSensors.removeCarHardwareLocationListener(carLocationListener)
|
||||||
|
carSensors.removeCompassListener(carCompassListener)
|
||||||
|
isLocationSensorActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds speed sensor if not already active.
|
||||||
|
*/
|
||||||
|
private fun addSpeedSensor() {
|
||||||
|
if (isSpeedSensorActive) return
|
||||||
|
|
||||||
|
if (carConnection == CarConnection.CONNECTION_TYPE_NATIVE ||
|
||||||
|
carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) {
|
||||||
|
val carInfo = carHardwareManager.carInfo
|
||||||
|
carInfo.addSpeedListener(carContext.mainExecutor, carSpeedListener)
|
||||||
|
isSpeedSensorActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes speed sensor.
|
||||||
|
*/
|
||||||
|
private fun removeSpeedSensor() {
|
||||||
|
if (!isSpeedSensorActive) return
|
||||||
|
|
||||||
|
val carInfo = carHardwareManager.carInfo
|
||||||
|
carInfo.removeSpeedListener(carSpeedListener)
|
||||||
|
isSpeedSensorActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up all sensor listeners.
|
||||||
|
* Should be called when the session is destroyed.
|
||||||
|
*/
|
||||||
|
fun cleanup() {
|
||||||
|
removeLocationSensors()
|
||||||
|
removeSpeedSensor()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
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 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 lifecycleOwner 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 DeviceLocationManager(
|
||||||
|
private val carContext: CarContext,
|
||||||
|
private val lifecycleOwner: LifecycleOwner,
|
||||||
|
private val shouldUseCarLocationFlow: Flow<Boolean>,
|
||||||
|
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 (location != null && shouldUseDeviceLocation) {
|
||||||
|
onLocationUpdate(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Observe car location setting to toggle device location usage
|
||||||
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
|
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
shouldUseCarLocationFlow.collectLatest { useCarLocation ->
|
||||||
|
shouldUseDeviceLocation = !useCarLocation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = 500, minDistanceM: Float = 5f) {
|
||||||
|
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,27 +1,16 @@
|
|||||||
package com.kouros.navigation.car
|
package com.kouros.navigation.car
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.location.LocationManager
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
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.hardware.CarHardwareManager
|
|
||||||
import androidx.car.app.hardware.common.CarValue
|
|
||||||
import androidx.car.app.hardware.common.OnCarDataAvailableListener
|
|
||||||
import androidx.car.app.hardware.info.CarHardwareLocation
|
|
||||||
import androidx.car.app.hardware.info.CarSensors
|
|
||||||
import androidx.car.app.hardware.info.Compass
|
|
||||||
import androidx.car.app.hardware.info.Speed
|
|
||||||
import androidx.core.location.LocationListenerCompat
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
@@ -45,11 +34,8 @@ import com.kouros.navigation.data.valhalla.ValhallaRepository
|
|||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
||||||
import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
||||||
import com.kouros.navigation.utils.getSettingsRepository
|
|
||||||
import kotlinx.coroutines.awaitCancellation
|
import kotlinx.coroutines.awaitCancellation
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import android.Manifest.permission
|
import android.Manifest.permission
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,7 +50,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
val useContacts = false
|
val useContacts = false
|
||||||
|
|
||||||
// Model for managing route state and navigation logic for Android Auto
|
// Model for managing route state and navigation logic for Android Auto
|
||||||
lateinit var routeModel: RouteCarModel;
|
lateinit var routeModel: RouteCarModel
|
||||||
|
|
||||||
// Main navigation screen displayed to the user
|
// Main navigation screen displayed to the user
|
||||||
lateinit var navigationScreen: NavigationScreen
|
lateinit var navigationScreen: NavigationScreen
|
||||||
@@ -72,41 +58,25 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
// Handles map surface rendering on the car display
|
// Handles map surface rendering on the car display
|
||||||
lateinit var surfaceRenderer: SurfaceRenderer
|
lateinit var surfaceRenderer: SurfaceRenderer
|
||||||
|
|
||||||
/**
|
// Manages car hardware sensors (location, compass, speed)
|
||||||
* Location listener that receives GPS updates from the device.
|
lateinit var carSensorManager: CarSensorManager
|
||||||
* Only processes location if car location hardware is not being used.
|
|
||||||
*/
|
// Manages device GPS location updates
|
||||||
var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? ->
|
lateinit var deviceLocationManager: DeviceLocationManager
|
||||||
val repository = getSettingsRepository(carContext)
|
|
||||||
val useCarLocation = runBlocking { repository.carLocationFlow.first() }
|
|
||||||
if (!useCarLocation) {
|
|
||||||
updateLocation(location!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 mLifeCycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
|
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
|
||||||
override fun onCreate(owner: LifecycleOwner) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume(owner: LifecycleOwner) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause(owner: LifecycleOwner) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy(owner: LifecycleOwner) {
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
removeSensors()
|
if (::carSensorManager.isInitialized) {
|
||||||
Log.i(TAG, "In onDestroy()")
|
carSensorManager.cleanup()
|
||||||
val locationManager =
|
}
|
||||||
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
if (::deviceLocationManager.isInitialized) {
|
||||||
locationManager.removeUpdates(mLocationListener)
|
deviceLocationManager.stopLocationUpdates()
|
||||||
|
}
|
||||||
|
Log.i(TAG, "NavigationSession destroyed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,48 +86,8 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
// Store for ViewModels to survive configuration changes
|
// Store for ViewModels to survive configuration changes
|
||||||
lateinit var viewModelStoreOwner : ViewModelStoreOwner
|
lateinit var viewModelStoreOwner : ViewModelStoreOwner
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for car hardware location updates.
|
|
||||||
* Receives location data from the car's GPS system.
|
|
||||||
*/
|
|
||||||
val carLocationListener: OnCarDataAvailableListener<CarHardwareLocation?> =
|
|
||||||
OnCarDataAvailableListener { data ->
|
|
||||||
if (data.location.status == CarValue.STATUS_SUCCESS) {
|
|
||||||
val location = data.location.value
|
|
||||||
if (location != null) {
|
|
||||||
updateLocation(location)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for car compass/orientation sensor.
|
|
||||||
* Updates the surface renderer with car orientation for map rotation.
|
|
||||||
*/
|
|
||||||
val carCompassListener: OnCarDataAvailableListener<Compass?> =
|
|
||||||
OnCarDataAvailableListener { data ->
|
|
||||||
if (data.orientations.status == CarValue.STATUS_SUCCESS) {
|
|
||||||
val orientation = data.orientations.value
|
|
||||||
if (orientation != null) {
|
|
||||||
surfaceRenderer.carOrientation = orientation[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for car speed sensor updates.
|
|
||||||
* Receives speed in meters per second from car hardware.
|
|
||||||
*/
|
|
||||||
val carSpeedListener = OnCarDataAvailableListener<Speed> { data ->
|
|
||||||
if (data.displaySpeedMetersPerSecond.status == CarValue.STATUS_SUCCESS) {
|
|
||||||
val speed = data.displaySpeedMetersPerSecond.value
|
|
||||||
surfaceRenderer.updateCarSpeed(speed!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val lifecycle: Lifecycle = lifecycle
|
lifecycle.addObserver(lifecycleObserver)
|
||||||
lifecycle.addObserver(mLifeCycleObserver)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -177,7 +107,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
* Initializes car hardware sensors if available.
|
* Initializes car hardware sensors if available.
|
||||||
*/
|
*/
|
||||||
fun onPermissionGranted(permission : Boolean) {
|
fun onPermissionGranted(permission : Boolean) {
|
||||||
addSensors(routeModel.navState.carConnection)
|
if (::carSensorManager.isInitialized) {
|
||||||
|
carSensorManager.updateConnectionState(routeModel.navState.carConnection)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,14 +119,16 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
*/
|
*/
|
||||||
fun onConnectionStateUpdated(connectionState: Int) {
|
fun onConnectionStateUpdated(connectionState: Int) {
|
||||||
routeModel.navState = routeModel.navState.copy(carConnection = connectionState)
|
routeModel.navState = routeModel.navState.copy(carConnection = connectionState)
|
||||||
|
if (::carSensorManager.isInitialized) {
|
||||||
|
carSensorManager.updateConnectionState(connectionState)
|
||||||
|
}
|
||||||
when (connectionState) {
|
when (connectionState) {
|
||||||
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not connected to a head unit"
|
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit
|
||||||
CarConnection.CONNECTION_TYPE_NATIVE -> {
|
CarConnection.CONNECTION_TYPE_NATIVE -> {
|
||||||
ObjectBox.init(carContext)
|
ObjectBox.init(carContext)
|
||||||
navigationScreen.checkPermission("android.car.permission.CAR_SPEED")
|
navigationScreen.checkPermission("android.car.permission.CAR_SPEED")
|
||||||
} // Automotive OS
|
}
|
||||||
CarConnection.CONNECTION_TYPE_PROJECTION -> {
|
CarConnection.CONNECTION_TYPE_PROJECTION -> {
|
||||||
"Connected to Android Auto"
|
|
||||||
navigationScreen.checkPermission("com.google.android.gms.permission.CAR_SPEED")
|
navigationScreen.checkPermission("com.google.android.gms.permission.CAR_SPEED")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,8 +140,17 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
* and returns appropriate starting screen.
|
* and returns appropriate starting screen.
|
||||||
*/
|
*/
|
||||||
override fun onCreateScreen(intent: Intent): Screen {
|
override fun onCreateScreen(intent: Intent): Screen {
|
||||||
|
setupViewModelStore()
|
||||||
|
initializeViewModels()
|
||||||
|
initializeManagers()
|
||||||
|
initializeScreen()
|
||||||
|
return checkPermissionsAndGetScreen()
|
||||||
|
}
|
||||||
|
|
||||||
// Create ViewModelStoreOwner to manage ViewModels across lifecycle
|
/**
|
||||||
|
* Sets up ViewModelStoreOwner and manages its lifecycle.
|
||||||
|
*/
|
||||||
|
private fun setupViewModelStore() {
|
||||||
viewModelStoreOwner = object : ViewModelStoreOwner {
|
viewModelStoreOwner = object : ViewModelStoreOwner {
|
||||||
override val viewModelStore = ViewModelStore()
|
override val viewModelStore = ViewModelStore()
|
||||||
}
|
}
|
||||||
@@ -218,96 +161,89 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
viewModelStoreOwner.viewModelStore.clear()
|
viewModelStoreOwner.viewModelStore.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize ViewModel with saved routing engine preference
|
/**
|
||||||
|
* Initializes ViewModels and observes their state changes.
|
||||||
|
*/
|
||||||
|
private fun initializeViewModels() {
|
||||||
navigationViewModel = getViewModel(carContext)
|
navigationViewModel = getViewModel(carContext)
|
||||||
|
|
||||||
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
|
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
|
||||||
|
|
||||||
navigationViewModel.permissionGranted.observe(this, ::onPermissionGranted)
|
navigationViewModel.permissionGranted.observe(this, ::onPermissionGranted)
|
||||||
|
|
||||||
routeModel = RouteCarModel()
|
routeModel = RouteCarModel()
|
||||||
|
|
||||||
// Monitor car connection state
|
|
||||||
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
|
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize surface renderer for map display
|
/**
|
||||||
|
* Initializes managers for rendering, sensors, and location.
|
||||||
|
*/
|
||||||
|
private fun initializeManagers() {
|
||||||
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
|
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
|
||||||
|
|
||||||
// Create main navigation screen
|
carSensorManager = CarSensorManager(
|
||||||
navigationScreen =
|
carContext = carContext,
|
||||||
NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel)
|
lifecycleOwner = this,
|
||||||
|
onLocationUpdate = ::updateLocation,
|
||||||
|
onCompassUpdate = { orientation -> surfaceRenderer.carOrientation = orientation },
|
||||||
|
onSpeedUpdate = { speed -> surfaceRenderer.updateCarSpeed(speed) }
|
||||||
|
)
|
||||||
|
|
||||||
// Check for required permissions before starting
|
deviceLocationManager = DeviceLocationManager(
|
||||||
if ( carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
|
carContext = carContext,
|
||||||
== PackageManager.PERMISSION_GRANTED
|
lifecycleOwner = this,
|
||||||
&& !useContacts
|
shouldUseCarLocationFlow = carSensorManager.shouldUseCarLocation(),
|
||||||
|| (useContacts && carContext.checkSelfPermission(Manifest.permission.READ_CONTACTS)
|
onLocationUpdate = ::updateLocation,
|
||||||
== PackageManager.PERMISSION_GRANTED)
|
onInitialLocation = { location ->
|
||||||
) {
|
navigationViewModel.loadRecentPlace(location, surfaceRenderer.carOrientation, carContext)
|
||||||
requestLocationUpdates()
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the main navigation screen.
|
||||||
|
*/
|
||||||
|
private fun initializeScreen() {
|
||||||
|
navigationScreen = NavigationScreen(
|
||||||
|
carContext,
|
||||||
|
surfaceRenderer,
|
||||||
|
routeModel,
|
||||||
|
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) {
|
||||||
|
deviceLocationManager.startLocationUpdates()
|
||||||
|
navigationScreen
|
||||||
} else {
|
} else {
|
||||||
// If we do not have the location permission, show the request permission screen.
|
showPermissionScreen()
|
||||||
val permissions: MutableList<String?> = ArrayList()
|
}
|
||||||
permissions.add(permission.ACCESS_FINE_LOCATION)
|
}
|
||||||
val screenManager =
|
|
||||||
carContext.getCarService(ScreenManager::class.java)
|
/**
|
||||||
screenManager
|
* Shows the permission request screen.
|
||||||
.push(navigationScreen)
|
*/
|
||||||
|
private fun showPermissionScreen(): Screen {
|
||||||
|
val screenManager = carContext.getCarService(ScreenManager::class.java)
|
||||||
|
screenManager.push(navigationScreen)
|
||||||
return RequestPermissionScreen(
|
return RequestPermissionScreen(
|
||||||
carContext,
|
carContext,
|
||||||
permissionCheckCallback = {
|
permissionCheckCallback = { screenManager.pop() }
|
||||||
screenManager.pop()
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return navigationScreen
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers listeners for car hardware sensors.
|
|
||||||
* Only adds location and compass sensors if useCarLocation setting is enabled.
|
|
||||||
* Speed sensor is added for both native and projection connections.
|
|
||||||
*/
|
|
||||||
fun addSensors(connectionState: Int) {
|
|
||||||
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
|
|
||||||
val repository = getSettingsRepository(carContext)
|
|
||||||
val useCarLocation = runBlocking { repository.carLocationFlow.first() }
|
|
||||||
if (useCarLocation) {
|
|
||||||
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
|
|
||||||
carSensors.addCompassListener(CarSensors.UPDATE_RATE_NORMAL,
|
|
||||||
carContext.mainExecutor,
|
|
||||||
carCompassListener)
|
|
||||||
carSensors.addCarHardwareLocationListener(
|
|
||||||
CarSensors.UPDATE_RATE_FASTEST,
|
|
||||||
carContext.mainExecutor,
|
|
||||||
carLocationListener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (connectionState == CarConnection.CONNECTION_TYPE_NATIVE
|
|
||||||
|| connectionState == CarConnection.CONNECTION_TYPE_PROJECTION) {
|
|
||||||
carInfo.addSpeedListener(carContext.mainExecutor, carSpeedListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregisters all car hardware sensor listeners.
|
|
||||||
* Called when session is being destroyed to prevent memory leaks.
|
|
||||||
*/
|
|
||||||
fun removeSensors() {
|
|
||||||
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
|
|
||||||
val repository = getSettingsRepository(carContext)
|
|
||||||
val useCarLocation = runBlocking { repository.carLocationFlow.first() }
|
|
||||||
if (useCarLocation) {
|
|
||||||
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
|
|
||||||
carSensors.removeCarHardwareLocationListener(carLocationListener)
|
|
||||||
}
|
|
||||||
if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_NATIVE
|
|
||||||
|| routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) {
|
|
||||||
carInfo.removeSpeedListener(carSpeedListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles new intents, primarily for navigation deep links from other apps.
|
* Handles new intents, primarily for navigation deep links from other apps.
|
||||||
@@ -315,99 +251,91 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
*/
|
*/
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
val screenManager = carContext.getCarService(ScreenManager::class.java)
|
val screenManager = carContext.getCarService(ScreenManager::class.java)
|
||||||
if ((CarContext.ACTION_NAVIGATE == intent.action)) {
|
|
||||||
val uri = ("http://" + intent.dataString).toUri()
|
|
||||||
val location = Location(LocationManager.GPS_PROVIDER)
|
|
||||||
screenManager.popToRoot()
|
|
||||||
screenManager.pushForResult(
|
|
||||||
SearchScreen(
|
|
||||||
carContext,
|
|
||||||
surfaceRenderer,
|
|
||||||
navigationViewModel
|
|
||||||
// TODO: Uri
|
|
||||||
)
|
|
||||||
) { obj: Any? ->
|
|
||||||
if (obj != null) {
|
|
||||||
|
|
||||||
}
|
// Handle Android Auto ACTION_NAVIGATE intent
|
||||||
}
|
if (CarContext.ACTION_NAVIGATE == intent.action) {
|
||||||
|
handleNavigateIntent(screenManager)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val uri = intent.data
|
// Handle custom deep links
|
||||||
if (uri != null && uriScheme == uri.scheme
|
handleDeepLink(intent, screenManager)
|
||||||
&& uriHost == uri.schemeSpecificPart
|
}
|
||||||
) {
|
|
||||||
val top = screenManager.getTop()
|
/**
|
||||||
|
* Handles ACTION_NAVIGATE intent by showing search screen.
|
||||||
|
*/
|
||||||
|
private fun handleNavigateIntent(screenManager: ScreenManager) {
|
||||||
|
screenManager.popToRoot()
|
||||||
|
screenManager.pushForResult(
|
||||||
|
SearchScreen(carContext, surfaceRenderer, navigationViewModel)
|
||||||
|
) { 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) {
|
when (uri.fragment) {
|
||||||
"DEEP_LINK_ACTION" -> if (top !is NavigationScreen) {
|
"DEEP_LINK_ACTION" -> {
|
||||||
|
if (screenManager.getTop() !is NavigationScreen) {
|
||||||
screenManager.popToRoot()
|
screenManager.popToRoot()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when car configuration changes (e.g., day/night mode).
|
|
||||||
*/
|
|
||||||
override fun onCarConfigurationChanged(newConfiguration: Configuration) {
|
|
||||||
println("Configuration: ${newConfiguration.isNightModeActive}")
|
|
||||||
super.onCarConfigurationChanged(newConfiguration)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests GPS location updates from the device.
|
|
||||||
* Updates with last known location and starts listening for updates every 500ms or 5 meters.
|
|
||||||
*/
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
fun requestLocationUpdates() {
|
|
||||||
val locationManager =
|
|
||||||
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
|
||||||
val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
|
|
||||||
if (location != null) {
|
|
||||||
navigationViewModel.loadRecentPlace(location = location, surfaceRenderer.carOrientation, carContext)
|
|
||||||
updateLocation(location)
|
|
||||||
locationManager.requestLocationUpdates(
|
|
||||||
LocationManager.GPS_PROVIDER,
|
|
||||||
/* minTimeMs= */ 500,
|
|
||||||
/* minDistanceM= */ 5f,
|
|
||||||
mLocationListener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates navigation state with new location.
|
* Updates navigation state with new location.
|
||||||
* Handles route snapping, deviation detection for rerouting, and map updates.
|
* Handles route snapping, deviation detection for rerouting, and map updates.
|
||||||
* Snaps location to nearest point on route if within threshold.
|
|
||||||
* Triggers reroute calculation if deviated too far from route.
|
|
||||||
*/
|
*/
|
||||||
fun updateLocation(location: Location) {
|
fun updateLocation(location: Location) {
|
||||||
|
updateBearing(location)
|
||||||
|
|
||||||
|
if (routeModel.isNavigating()) {
|
||||||
|
handleNavigationLocation(location)
|
||||||
|
} else {
|
||||||
|
surfaceRenderer.updateLocation(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates route bearing if location has bearing information.
|
||||||
|
*/
|
||||||
|
private fun updateBearing(location: Location) {
|
||||||
if (location.hasBearing()) {
|
if (location.hasBearing()) {
|
||||||
routeModel.navState = routeModel.navState.copy(routeBearing = location.bearing)
|
routeModel.navState = routeModel.navState.copy(routeBearing = location.bearing)
|
||||||
}
|
}
|
||||||
if (routeModel.isNavigating()) {
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles location updates during active navigation.
|
||||||
|
* Snaps location to route and checks for deviation requiring reroute.
|
||||||
|
*/
|
||||||
|
private fun handleNavigationLocation(location: Location) {
|
||||||
navigationScreen.updateTrip(location)
|
navigationScreen.updateTrip(location)
|
||||||
if (!routeModel.navState.arrived) {
|
|
||||||
val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations())
|
if (routeModel.navState.arrived) return
|
||||||
val distance = location.distanceTo(snapedLocation)
|
|
||||||
// Check if user has deviated too far from route
|
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
|
||||||
if (distance > MAXIMAL_ROUTE_DEVIATION) {
|
val distance = location.distanceTo(snappedLocation)
|
||||||
|
|
||||||
|
when {
|
||||||
|
distance > MAXIMAL_ROUTE_DEVIATION -> {
|
||||||
navigationScreen.calculateNewRoute(routeModel.navState.destination)
|
navigationScreen.calculateNewRoute(routeModel.navState.destination)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// Snap to route if close enough, otherwise use raw location
|
distance < MAXIMAL_SNAP_CORRECTION -> {
|
||||||
if (distance < MAXIMAL_SNAP_CORRECTION) {
|
surfaceRenderer.updateLocation(snappedLocation)
|
||||||
surfaceRenderer.updateLocation(snapedLocation)
|
}
|
||||||
} else {
|
else -> {
|
||||||
surfaceRenderer.updateLocation(location)
|
surfaceRenderer.updateLocation(location)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
surfaceRenderer.updateLocation(location)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -33,11 +33,9 @@ import com.kouros.navigation.car.navigation.RouteCarModel
|
|||||||
import com.kouros.navigation.data.Constants
|
import com.kouros.navigation.data.Constants
|
||||||
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
|
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.data.datastore.DataStoreManager
|
|
||||||
import com.kouros.navigation.data.nominatim.SearchResult
|
import com.kouros.navigation.data.nominatim.SearchResult
|
||||||
import com.kouros.navigation.data.overpass.Elements
|
import com.kouros.navigation.data.overpass.Elements
|
||||||
import com.kouros.navigation.model.NavigationViewModel
|
import com.kouros.navigation.model.NavigationViewModel
|
||||||
import com.kouros.navigation.repository.SettingsRepository
|
|
||||||
import com.kouros.navigation.utils.GeoUtils
|
import com.kouros.navigation.utils.GeoUtils
|
||||||
import com.kouros.navigation.utils.getSettingsViewModel
|
import com.kouros.navigation.utils.getSettingsViewModel
|
||||||
import com.kouros.navigation.utils.location
|
import com.kouros.navigation.utils.location
|
||||||
@@ -561,7 +559,6 @@ class NavigationScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun checkPermission(permission: String) {
|
fun checkPermission(permission: String) {
|
||||||
println("Car connection permission: $permission")
|
|
||||||
if (carContext.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
|
if (carContext.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
|
||||||
val permissions: MutableList<String?> = ArrayList()
|
val permissions: MutableList<String?> = ArrayList()
|
||||||
permissions.add(permission)
|
permissions.add(permission)
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class NavigationSettings(
|
|||||||
)
|
)
|
||||||
.addItem(
|
.addItem(
|
||||||
buildRowForScreenTemplate(
|
buildRowForScreenTemplate(
|
||||||
PasswordSettings(carContext, navigationViewModel),
|
PasswordSettings(carContext),
|
||||||
R.string.tomtom_api_key
|
R.string.tomtom_api_key
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import com.kouros.navigation.utils.getSettingsViewModel
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class PasswordSettings(
|
class PasswordSettings(
|
||||||
private val carContext: CarContext,
|
private val carContext: CarContext
|
||||||
private var navigationViewModel: NavigationViewModel
|
|
||||||
) : Screen(carContext) {
|
) : Screen(carContext) {
|
||||||
|
|
||||||
var errorMessage: String? = null
|
var errorMessage: String? = null
|
||||||
@@ -51,7 +50,6 @@ class PasswordSettings(
|
|||||||
val pinSignInAction = Action.Builder()
|
val pinSignInAction = Action.Builder()
|
||||||
.setTitle(carContext.getString(R.string.stop_action_title))
|
.setTitle(carContext.getString(R.string.stop_action_title))
|
||||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||||
println("Sign")
|
|
||||||
invalidate()
|
invalidate()
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.kouros.navigation.data
|
||||||
|
|
||||||
|
import android.location.Location
|
||||||
|
import com.kouros.navigation.model.IconMapper
|
||||||
|
import com.kouros.navigation.utils.location
|
||||||
|
|
||||||
|
// Immutable Data Class
|
||||||
|
data class NavigationState (
|
||||||
|
val route: Route = Route.Builder().buildEmpty(),
|
||||||
|
val iconMapper: IconMapper = IconMapper(),
|
||||||
|
val navigating: Boolean = false,
|
||||||
|
val arrived: Boolean = false,
|
||||||
|
val travelMessage: String = "",
|
||||||
|
val maneuverType: Int = 0,
|
||||||
|
val lastLocation: Location = location(0.0, 0.0),
|
||||||
|
val currentLocation: Location = location(0.0, 0.0),
|
||||||
|
val routeBearing: Float = 0F,
|
||||||
|
val currentRouteIndex: Int = 0,
|
||||||
|
val destination: Place = Place(),
|
||||||
|
val carConnection: Int = 0,
|
||||||
|
)
|
||||||
@@ -37,7 +37,7 @@ class TomTomRepository : NavigationRepository() {
|
|||||||
val tomtomApiKey = runBlocking { repository.tomTomApiKeyFlow.first() }
|
val tomtomApiKey = runBlocking { repository.tomTomApiKeyFlow.first() }
|
||||||
val url =
|
val url =
|
||||||
routeUrl + "${currentLocation.latitude},${currentLocation.longitude}:${location.latitude},${location.longitude}" +
|
routeUrl + "${currentLocation.latitude},${currentLocation.longitude}:${location.latitude},${location.longitude}" +
|
||||||
"/json?vehicleHeading=90§ionType=traffic&report=effectiveSettings&routeType=eco" +
|
"/json?sectionType=traffic&report=effectiveSettings&routeType=eco" +
|
||||||
"&traffic=true&avoid=unpavedRoads&travelMode=car" +
|
"&traffic=true&avoid=unpavedRoads&travelMode=car" +
|
||||||
"&vehicleMaxSpeed=120&vehicleCommercial=false" +
|
"&vehicleMaxSpeed=120&vehicleCommercial=false" +
|
||||||
"&instructionsType=text&language=en-GB§ionType=lanes" +
|
"&instructionsType=text&language=en-GB§ionType=lanes" +
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_NATIVE
|
|||||||
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_PROJECTION
|
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_PROJECTION
|
||||||
import androidx.car.app.navigation.model.Maneuver
|
import androidx.car.app.navigation.model.Maneuver
|
||||||
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
|
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
|
||||||
|
import com.kouros.navigation.data.NavigationState
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.data.Route
|
import com.kouros.navigation.data.Route
|
||||||
import com.kouros.navigation.data.StepData
|
import com.kouros.navigation.data.StepData
|
||||||
@@ -23,22 +24,6 @@ import kotlin.math.absoluteValue
|
|||||||
|
|
||||||
open class RouteModel {
|
open class RouteModel {
|
||||||
|
|
||||||
// Immutable Data Class
|
|
||||||
data class NavigationState(
|
|
||||||
val route: Route = Route.Builder().buildEmpty(),
|
|
||||||
val iconMapper: IconMapper = IconMapper(),
|
|
||||||
val navigating: Boolean = false,
|
|
||||||
val arrived: Boolean = false,
|
|
||||||
val travelMessage: String = "",
|
|
||||||
val maneuverType: Int = 0,
|
|
||||||
val lastLocation: Location = location(0.0, 0.0),
|
|
||||||
val currentLocation: Location = location(0.0, 0.0),
|
|
||||||
val routeBearing: Float = 0F,
|
|
||||||
val currentRouteIndex: Int = 0,
|
|
||||||
val destination: Place = Place(),
|
|
||||||
val carConnection: Int = 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
var navState = NavigationState()
|
var navState = NavigationState()
|
||||||
|
|
||||||
val route: Route
|
val route: Route
|
||||||
|
|||||||
@@ -52,5 +52,7 @@
|
|||||||
<string name="options">Optionen</string>
|
<string name="options">Optionen</string>
|
||||||
<string name="tomtom_api_key">TomTom ApiKey</string>
|
<string name="tomtom_api_key">TomTom ApiKey</string>
|
||||||
<string name="use_car_settings">Verwende Auto Einstellungen</string>
|
<string name="use_car_settings">Verwende Auto Einstellungen</string>
|
||||||
|
<string name="exit_number">Ausfahrt nummer</string>
|
||||||
|
<string name="navigation_icon_description">Navigations Icon</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -38,4 +38,6 @@
|
|||||||
<string name="options">Options</string>
|
<string name="options">Options</string>
|
||||||
<string name="tomtom_api_key">TomTom ApiKey</string>
|
<string name="tomtom_api_key">TomTom ApiKey</string>
|
||||||
<string name="use_car_settings">Use car settings</string>
|
<string name="use_car_settings">Use car settings</string>
|
||||||
|
<string name="exit_number">Exit number</string>
|
||||||
|
<string name="navigation_icon_description">Navigation icon</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user