Launcher Icons, NotificationService

This commit is contained in:
Dimitris
2026-03-24 17:04:05 +01:00
parent bc8a53a5d8
commit 5098dad9d6
76 changed files with 930 additions and 478 deletions

View File

@@ -29,6 +29,7 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
}
@@ -52,7 +53,8 @@ dependencies {
implementation(libs.play.services.location)
implementation(libs.androidx.datastore.core)
implementation(libs.androidx.monitor)
implementation(libs.android.gpx.parser)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.runner)
androidTestImplementation(libs.androidx.rules)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -137,7 +137,7 @@ class CarSensorManager(
carCompassListener
)
carSensors.addCarHardwareLocationListener(
CarSensors.UPDATE_RATE_FASTEST,
CarSensors.UPDATE_RATE_NORMAL,
carContext.mainExecutor,
carLocationListener
)

View File

@@ -1,7 +1,7 @@
package com.kouros.navigation.car
import android.annotation.SuppressLint
import android.location.Location
import android.net.Uri
import androidx.car.app.CarAppService
import androidx.car.app.Session
import androidx.car.app.SessionInfo
@@ -10,6 +10,14 @@ import androidx.car.app.validation.HostValidator
class NavigationCarAppService : CarAppService() {
val INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP =
"com.kouros.navigation.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP"
fun createDeepLinkUri(deepLinkAction: String): Uri {
return Uri.fromParts(NavigationSession.uriScheme, NavigationSession.uriHost, deepLinkAction)
}
@SuppressLint("PrivateResource")
override fun createHostValidator(): HostValidator {

View File

@@ -0,0 +1,232 @@
package com.kouros.navigation.car
import android.annotation.SuppressLint
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.Message
import androidx.car.app.notification.CarAppExtender
import androidx.car.app.notification.CarNotificationManager
import androidx.car.app.notification.CarPendingIntent
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.kouros.data.R
import java.math.RoundingMode
import java.text.DecimalFormat
import java.util.concurrent.TimeUnit
/**
* A simple foreground service that imitates a client routing service posting navigation
* notifications.
*/
class NavigationNotificationService : Service() {
/**
* The number of notifications fired so far.
*
*
* We use this number to post notifications with a repeating list of directions. See [ ][.getDirectionInfo] for details.
*
* Note: Package private for inner class reference
*/
var mNotificationCount: Int = 0
/**
* A handler that posts notifications when given the message request. See [ ] for details.
*
* Note: Package private for inner class reference
*/
val mHandler: Handler =
Handler(Looper.getMainLooper(), HandlerCallback())
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
initNotifications(this)
startForeground(
NAV_NOTIFICATION_ID,
getNavigationNotification(this, mNotificationCount).build()
)
// Start updating the notification continuously.
mHandler.sendMessageDelayed(
mHandler.obtainMessage(MSG_SEND_NOTIFICATION), NAV_NOTIFICATION_DELAY_IN_MILLIS
)
return START_NOT_STICKY
}
override fun onDestroy() {
mHandler.removeMessages(MSG_SEND_NOTIFICATION)
}
override fun onBind(intent: Intent): IBinder? {
return null
}
/**
* A [Handler.Callback] used to process the message queue for the notification service.
*/
internal inner class HandlerCallback : Handler.Callback {
override fun handleMessage(msg: Message): Boolean {
if (msg.what == MSG_SEND_NOTIFICATION) {
val context: Context = this@NavigationNotificationService
CarNotificationManager.from(context).notify(
NAV_NOTIFICATION_ID,
getNavigationNotification(context, mNotificationCount)
)
mNotificationCount++
mHandler.sendMessageDelayed(
mHandler.obtainMessage(MSG_SEND_NOTIFICATION),
NAV_NOTIFICATION_DELAY_IN_MILLIS
)
return true
}
return false
}
}
/**
* A container class that encapsulates the direction information to use in the notifications.
*/
internal class DirectionInfo(
val mTitle: String, val mDistance: String, val mIcon: Int,
val mOnlyAlertOnce: Boolean
)
companion object {
private const val MSG_SEND_NOTIFICATION = 1
private const val NAV_NOTIFICATION_CHANNEL_ID = "nav_channel_00"
private val NAV_NOTIFICATION_CHANNEL_NAME: CharSequence = "Navigation Channel"
private const val NAV_NOTIFICATION_ID = 10101
val NAV_NOTIFICATION_DELAY_IN_MILLIS: Long = TimeUnit.SECONDS.toMillis(1)
/**
* Initializes the notifications, if needed.
*
*
* [NotificationManager.IMPORTANCE_HIGH] is needed to show the alerts on top of the car
* screen. However, the rail widget at the bottom of the screen will show regardless of the
* importance setting.
*/
// Suppressing 'ObsoleteSdkInt' as this code is shared between APKs with different min SDK
// levels
@SuppressLint("ObsoleteSdkInt")
private fun initNotifications(context: Context) {
val navChannel =
NotificationChannelCompat.Builder(
NAV_NOTIFICATION_CHANNEL_ID,
NotificationManagerCompat.IMPORTANCE_HIGH
)
.setName(NAV_NOTIFICATION_CHANNEL_NAME).build()
CarNotificationManager.from(context).createNotificationChannel(navChannel)
}
/** Returns the navigation notification that corresponds to the given notification count. */
fun getNavigationNotification(
context: Context, notificationCount: Int
): NotificationCompat.Builder {
val builder =
NotificationCompat.Builder(context, NAV_NOTIFICATION_CHANNEL_ID)
val directionInfo = getDirectionInfo(context, notificationCount)
// Set an intent to open the car app. The app receives this intent when the user taps the
// heads-up notification or the rail widget.
val pendingIntent = CarPendingIntent.getCarApp(
context,
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP.hashCode(),
Intent(
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP
).setComponent(
ComponentName(
context,
NavigationCarAppService()::class.java
)
).setData(
NavigationCarAppService().createDeepLinkUri(
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP
)
),
0
)
return builder // This title, text, and icon will be shown in both phone and car screen. These
// values can
// be overridden in the extender below, to customize notifications in the car
// screen.
.setContentTitle(directionInfo.mTitle)
.setContentText(directionInfo.mDistance)
.setSmallIcon(directionInfo.mIcon) // The notification must be set to 'ongoing' and its category must be set to
// CATEGORY_NAVIGATION in order to show it in the rail widget when the app is
// navigating on
// the background.
// These values cannot be overridden in the extender.
.setOngoing(true)
.setCategory(NotificationCompat.CATEGORY_NAVIGATION) // If set to true, the notification will only show the alert once in both phone and
// car screen. This value cannot be overridden in the extender.
.setOnlyAlertOnce(directionInfo.mOnlyAlertOnce) // This extender must be set in order to display the notification in the car screen.
// The extender also allows various customizations, such as showing different title
// or icon on the car screen.
.extend(
CarAppExtender.Builder()
.setContentIntent(pendingIntent)
.build()
)
}
/**
* Returns a [DirectionInfo] that corresponds to the given notification count.
*
*
* There are 5 directions, repeating in order. For each direction, the alert will only show
* once, but the distance will update on every count on the rail widget.
*/
private fun getDirectionInfo(context: Context, notificationCount: Int): DirectionInfo {
val formatter = DecimalFormat("#.##")
formatter.setRoundingMode(RoundingMode.DOWN)
val repeatingCount = notificationCount % 35
if (repeatingCount in 0..<10) {
// Distance decreases from 1km to 0.1km
val distance = formatter.format((10 - repeatingCount) * 0.1) + "km"
return DirectionInfo(
context.getString(R.string.stop_action_title),
distance,
R.drawable.arrow_back_24px,
repeatingCount > 0
)
} else if (repeatingCount in 10..<20) {
// Distance decreases from 5km to 0.5km
val distance = formatter.format((20 - repeatingCount) * 0.5) + "km"
return DirectionInfo(
context.getString(R.string.route_preview),
distance,
R.drawable.ic_turn_normal_right, /* onlyAlertOnce= */
repeatingCount > 10
)
} else if (repeatingCount in 20..<25) {
// Distance decreases from 200m to 40m
val distance = formatter.format(((25 - repeatingCount) * 40).toLong()) + "m"
return DirectionInfo(
context.getString(R.string.route_preview),
distance,
R.drawable.navigation_48px, /* onlyAlertOnce= */
repeatingCount > 20
)
} else {
// Distance decreases from 1km to 0.1km
val distance = formatter.format((35 - repeatingCount) * 0.1) + "km"
return DirectionInfo(
context.getString(R.string.charging_station),
distance,
R.drawable.local_gas_station_24,
repeatingCount > 25
)
}
}
}
}

View File

@@ -26,6 +26,7 @@ import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.navigation.Simulation
import com.kouros.navigation.car.screen.NavigationListener
import com.kouros.navigation.car.screen.NavigationScreen
import com.kouros.navigation.car.screen.NavigationType
import com.kouros.navigation.car.screen.RequestPermissionScreen
import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.car.screen.checkPermission
@@ -37,6 +38,7 @@ import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
@@ -229,9 +231,10 @@ class NavigationSession : Session(), NavigationListener {
override fun onStopNavigation() {
// Called when the user stops navigation in the car screen
// Stop turn-by-turn logic and clean up
routeModel.stopNavigation()
autoDriveEnabled = false
deviceLocationManager.startLocationUpdates()
stopNavigation()
if (autoDriveEnabled) {
deviceLocationManager.startLocationUpdates()
}
}
})
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
@@ -387,6 +390,7 @@ class NavigationSession : Session(), NavigationListener {
* Snaps location to route and checks for deviation requiring reroute.
*/
private fun handleNavigationLocation(location: Location) {
if (guidanceAudio == 1) {
handleGuidanceAudio()
}
@@ -420,6 +424,9 @@ class NavigationSession : Session(), NavigationListener {
simulation.stopSimulation()
autoDriveEnabled = false
}
surfaceRenderer.routeData.value = ""
surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationScreen.navigationType = NavigationType.VIEW
}
/**

View File

@@ -10,12 +10,17 @@ import androidx.car.app.AppManager
import androidx.car.app.CarContext
import androidx.car.app.SurfaceCallback
import androidx.car.app.SurfaceContainer
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
@@ -29,9 +34,11 @@ import com.kouros.navigation.car.map.MapLibre
import com.kouros.navigation.car.map.cameraState
import com.kouros.navigation.car.map.getPaddingValues
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateTilt
@@ -45,9 +52,10 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.expressions.dsl.zoom
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position
import java.time.Duration
import java.time.LocalDateTime
/**
@@ -123,6 +131,8 @@ class SurfaceRenderer(
// Camera tilt angle (default 60 degrees for navigation)
var tilt = TILT
var lastLocationUpdate: LocalDateTime = LocalDateTime.now()
// Map base style (day/night)
val style: MutableLiveData<BaseStyle> by lazy {
MutableLiveData()
@@ -238,7 +248,6 @@ class SurfaceRenderer(
init {
lifecycle.addObserver(this)
speed.value = 0F
}
fun onBaseStyleStateUpdated(style: BaseStyle) {
@@ -287,7 +296,7 @@ class SurfaceRenderer(
darkMode: Boolean
) {
val cameraDuration =
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing)
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing, lastLocationUpdate)
val currentSpeed: Float? by speed.observeAsState()
val maximumSpeed: Int? by maxSpeed.observeAsState()
val streetName: String? by street.observeAsState()
@@ -311,9 +320,10 @@ class SurfaceRenderer(
tilt = tilt,
padding = paddingValues
),
duration = cameraDuration
duration = cameraDuration,
)
}
lastLocationUpdate = LocalDateTime.now()
}
override fun onCreate(owner: LifecycleOwner) {
@@ -396,6 +406,15 @@ class SurfaceRenderer(
viewStyle = ViewStyle.VIEW
}
/**
* Activates navigation View
*/
fun activateNavigationView() {
viewStyle = ViewStyle.VIEW
tilt = TILT
updateLocation(lastLocation)
}
/**
* Updates camera position with new bearing, zoom, and target.
* Posts update to LiveData for UI observation.
@@ -414,21 +433,6 @@ class SurfaceRenderer(
}
}
/**
* Sets route data for active navigation and switches to VIEW mode.
*/
fun clearRouteData() {
updateLocation(lastLocation)
routeData.value = ""
viewStyle = ViewStyle.VIEW
cameraPosition.postValue(
cameraPosition.value!!.copy(
zoom = 16.0
)
)
tilt = TILT
}
/**
* Updates traffic incident data on the map.
*/
@@ -492,9 +496,9 @@ class SurfaceRenderer(
}
/**
* Centers the map on a specific category/POI location.
* Centers the map on a specific POI location.
*/
fun setCategoryLocation(location: Location, category: String) {
fun setCategoryLocation(location: Location) {
viewStyle = ViewStyle.AMENITY_VIEW
cameraPosition.postValue(
cameraPosition.value!!.copy(
@@ -502,22 +506,4 @@ class SurfaceRenderer(
)
)
}
companion
object {
private const val TAG = "MapRenderer"
}
}
/**
* Enum representing different map view modes.
* - VIEW: Active navigation mode with follow-car camera
* - PREVIEW: Route overview before starting navigation
* - PAN_VIEW: User-controlled map panning
* - AMENITY_VIEW: Displaying POI/amenity locations
*/
enum class ViewStyle {
VIEW, PREVIEW, PAN_VIEW, AMENITY_VIEW
}

View File

@@ -26,11 +26,12 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.kouros.data.R
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.NavigationColor
import com.kouros.navigation.data.NavigationColorDark
import com.kouros.navigation.data.NavigationColorLight
import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.utils.isMetricSystem
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
@@ -74,9 +75,9 @@ fun cameraState(
latitude = position!!.target.latitude,
longitude = position.target.longitude
),
zoom = 15.0,
zoom = position.zoom,
tilt = tilt,
padding = padding
padding = padding,
)
)
}
@@ -317,7 +318,10 @@ fun NavigationImage(
) {
val imageSize = (height / 8)
val navigationColor = remember { NavigationColor }
val navigationColor = if (darkMode)
remember { NavigationColorDark }
else
remember { NavigationColorLight }
val textMeasurerStreet = rememberTextMeasurer()
val street = streetName.toString()
@@ -545,7 +549,7 @@ fun DebugInfo(
fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues {
return when (viewStyle) {
ViewStyle.VIEW, ViewStyle.PAN_VIEW -> PaddingValues(
start = 50.dp,
start = 100.dp,
top = distanceFromTop(height).dp
)

View File

@@ -1,80 +0,0 @@
package com.kouros.navigation.car.navigation
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.media.MediaPlayer
import android.media.MediaPlayer.OnCompletionListener
import android.speech.tts.TextToSpeech
import android.util.Log
import androidx.annotation.DrawableRes
import androidx.annotation.RawRes
import androidx.annotation.StringRes
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.model.Action
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText
import androidx.car.app.model.Row
import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R
import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY
import com.kouros.navigation.data.Constants.TAG
import java.io.IOException
import java.util.Locale
class NavigationUtils(private var carContext: CarContext) {
fun createCarIcon(@DrawableRes iconRes: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
}
fun buildRowForTemplate(title: Int, resource: Int): Row {
return Row.Builder()
.setTitle(carContext.getString(title))
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
resource
)
)
.build()
)
.build()
}
fun createNumberIcon(category: String, number: String): IconCompat {
val size = 24
val bitmap = createBitmap(size, size)
val canvas = Canvas(bitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textSize = size * 0.7f
textAlign = Paint.Align.CENTER
isFakeBoldText = true
}
val xPos = size / 2f
val yPos = (size / 2f) - ((paint.descent() + paint.ascent()) / 2f)
val color = when (category) {
CHARGING_STATION -> Color.GREEN
FUEL_STATION -> Color.BLUE
PHARMACY -> Color.RED
else -> Color.WHITE
}
paint.color = color
canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
paint.color = Color.WHITE
canvas.drawText(number, xPos, yPos, paint)
return IconCompat.createWithBitmap(bitmap)
}
}

View File

@@ -3,6 +3,7 @@ package com.kouros.navigation.car.navigation
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.util.Log
import androidx.annotation.StringRes
import androidx.car.app.AppManager
import androidx.car.app.CarContext
@@ -184,7 +185,6 @@ class RouteCarModel : RouteModel() {
return CarText.create(carContext.getString(stringRes))
}
fun createCarIcon(iconCompat: IconCompat): CarIcon {
return CarIcon.Builder(iconCompat).build()
}
@@ -235,4 +235,17 @@ class RouteCarModel : RouteModel() {
.setFlags(flags)
.build()
}
fun backGroundColor(): CarColor {
return if (isNavigating()) {
when (route.currentStep().countryCode) {
"DEU", "FRA", "AUT", "POL", "BEL", "NLD", "ESP", "PRT", "CZE", "SVK", "BGR", "HUN" -> CarColor.BLUE
else -> {
CarColor.GREEN
}
}
} else {
CarColor.GREEN
}
}
}

View File

@@ -4,47 +4,126 @@ import android.location.Location
import android.location.LocationManager
import android.os.SystemClock
import androidx.lifecycle.LifecycleCoroutineScope
import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.utils.location
import com.kouros.android.cars.carappservice.BuildConfig
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.joda.time.DateTime
class Simulation {
private var simulationJob: Job? = null
fun startSimulation(
routeModel: RouteCarModel,
lifecycleScope: LifecycleCoroutineScope,
updateLocation: (Location) -> Unit
) {
if (routeModel.navState.route.isRouteValid()) {
val points = routeModel.curRoute.waypoints
if (points.isEmpty()) return
simulationJob?.cancel()
if (BuildConfig.DEBUG) {
gpxSimulation(routeModel, lifecycleScope, updateLocation)
} else {
currentSimulation(routeModel, lifecycleScope, updateLocation)
}
return
}
}
private fun currentSimulation(
routeModel: RouteCarModel,
lifecycleScope: LifecycleCoroutineScope,
updateLocation: (Location) -> Unit
) {
val points = routeModel.curRoute.waypoints
if (points.isEmpty()) return
simulationJob?.cancel()
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
var curBearing = 0f
simulationJob = lifecycleScope.launch {
for (point in points) {
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
latitude = point[1]
longitude = point[0]
bearing = curBearing
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
speed = 13.0f // ~50 km/h
time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
}
curBearing = lastLocation.bearingTo(fakeLocation)
// 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)
delay(1000)
lastLocation = fakeLocation
}
routeModel.stopNavigation()
}
}
private fun gpxSimulation(
routeModel: RouteCarModel,
lifecycleScope: LifecycleCoroutineScope,
updateLocation: (Location) -> Unit
) {
var route = ""
simulationJob?.cancel()
runBlocking {
simulationJob = launch(Dispatchers.IO) {
route = TomTomRepository().fetchUrl(
"https://kouros-online.de/vh.gpx",
false
)
}
simulationJob?.join()
}
simulationJob?.cancel()
simulationJob =lifecycleScope.launch() {
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
var curBearing = 0f
simulationJob = lifecycleScope.launch {
for (point in points) {
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
latitude = point[1]
longitude = point[0]
bearing = curBearing
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
speed = 13.0f // ~50 km/h
time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
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)
}
lastTime = p.time
lastLocation = fakeLocation
}
}
curBearing = lastLocation.bearingTo(fakeLocation)
// 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)
delay(500)
lastLocation = fakeLocation
}
routeModel.stopNavigation()
}
routeModel.stopNavigation()
}
}

View File

@@ -1,6 +1,5 @@
package com.kouros.navigation.car.screen
import androidx.activity.OnBackPressedCallback
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
@@ -13,18 +12,12 @@ import androidx.car.app.model.Template
import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.screen.observers.CategoryObserver
import com.kouros.navigation.car.screen.observers.CategoryObserverCallback
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.utils.GeoUtils.createPointCollection
import com.kouros.navigation.utils.location
class CategoriesScreen(
private val carContext: CarContext,
@@ -100,7 +93,7 @@ fun carIcon(context: CarContext, category: String, index: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(context, resId)).build()
} else {
return CarIcon.Builder(
NavigationUtils(context).createNumberIcon(
createNumberIcon(
category,
index.toString()
)

View File

@@ -21,7 +21,6 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.screen.observers.CategoryObserver
import com.kouros.navigation.car.screen.observers.CategoryObserverCallback
import com.kouros.navigation.data.Constants
@@ -130,7 +129,7 @@ class CategoryScreen(
val row = Row.Builder()
.setOnClickListener {
val location = location(it.lon, it.lat)
surfaceRenderer.setCategoryLocation(location, category)
surfaceRenderer.setCategoryLocation(location)
}
.setTitle(name)
.setImage(carIcon(carContext, category, index))

View File

@@ -9,7 +9,6 @@ import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.model.Header
@@ -31,28 +30,25 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.screen.observers.NavigationObserverCallback
import com.kouros.navigation.car.screen.observers.NavigationObserverManager
import com.kouros.navigation.car.screen.settings.SettingsScreen
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.maplibre.spatialk.geojson.Position
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneOffset
@@ -70,8 +66,6 @@ open class NavigationScreen(
private val navigationViewModel: NavigationViewModel
) : Screen(carContext), NavigationObserverCallback {
val backGroundColor = CarColor.GREEN
var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER)
var recentPlaces = mutableListOf<Place>()
@@ -191,7 +185,7 @@ open class NavigationScreen(
)
})
)
.setBackgroundColor(backGroundColor)
.setBackgroundColor(routeModel.backGroundColor())
.build()
}
@@ -200,7 +194,7 @@ open class NavigationScreen(
*/
private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template {
return NavigationTemplate.Builder()
.setBackgroundColor(backGroundColor)
.setBackgroundColor(routeModel.backGroundColor())
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(
mapActionStrip(
@@ -261,7 +255,7 @@ open class NavigationScreen(
)
.build()
)
.setBackgroundColor(backGroundColor)
.setBackgroundColor(routeModel.backGroundColor())
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(
mapActionStrip(
@@ -298,7 +292,7 @@ open class NavigationScreen(
)
}
val listBuilder = ItemList.Builder()
recentPlaces.filter { it.category == Constants.RECENT }.forEach {
recentPlaces.filter { it.category == Constants.RECENT && it.distance > 300F }.forEach {
val row = Row.Builder()
.setTitle(it.name!!)
.addAction(
@@ -355,7 +349,7 @@ open class NavigationScreen(
return NavigationTemplate.Builder()
.setNavigationInfo(RoutingInfo.Builder().setLoading(true).build())
.setActionStrip(actionStripBuilder.build())
.setBackgroundColor(backGroundColor)
.setBackgroundColor(routeModel.backGroundColor())
.build()
}
@@ -421,12 +415,6 @@ open class NavigationScreen(
* Creates an action to start the settings screen.
*/
private fun settingsAction(): Action {
// return Action.Builder()
// .setIcon(createCarIcon(carContext, R.drawable.settings_48px))
// .setOnClickListener {
// screenManager.push(SettingsScreen(carContext, navigationViewModel))
// }
// .build()
return createAction(
carContext, R.drawable.settings_48px,
0,
@@ -514,13 +502,7 @@ open class NavigationScreen(
navigationViewModel.route.value = preview
}
routeModel.navState = routeModel.navState.copy(destination = place)
surfaceRenderer.viewStyle = ViewStyle.VIEW
surfaceRenderer.updateCameraPosition(
0.0,
16.0,
Position(surfaceRenderer.lastLocation.longitude, surfaceRenderer.lastLocation.latitude),
TILT
)
surfaceRenderer.activateNavigationView()
invalidate()
}
@@ -530,7 +512,6 @@ open class NavigationScreen(
fun stopNavigation() {
navigationType = NavigationType.VIEW
listener.stopNavigation()
surfaceRenderer.routeData.value = ""
lastCameraSearch = 0
invalidate()
}
@@ -573,9 +554,9 @@ open class NavigationScreen(
* Updates navigation state with the current location, checks for arrival, and traffic updates.
*/
fun updateTrip(location: Location) {
val current = LocalDateTime.now(ZoneOffset.UTC)
checkRoute(current, location)
checkTraffic(current, location)
val currentDate = LocalDateTime.now(ZoneOffset.UTC)
checkRoute(currentDate, location)
checkTraffic(currentDate, location)
updateSpeedCamera(location)
@@ -588,11 +569,11 @@ open class NavigationScreen(
/**
* Checks if a new route is needed based on the time since the last update.
*/
private fun checkRoute(current: LocalDateTime, location: Location) {
val duration = Duration.between(current, lastRouteDate)
val routeUpdate = routeModel.curRoute.summary.duration / 6
private fun checkRoute(currentDate: LocalDateTime, location: Location) {
val duration = Duration.between(currentDate, lastRouteDate)
val routeUpdate = routeModel.curRoute.summary.duration / 4
if (duration.abs().seconds > routeUpdate) {
lastRouteDate = current
lastRouteDate = currentDate
val destination = location(
routeModel.navState.destination.longitude,
routeModel.navState.destination.latitude

View File

@@ -36,10 +36,9 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsRepository
@@ -324,7 +323,7 @@ class RoutePreviewScreen(
.addAction(navigateAction)
if (route.summary.trafficDelay > 60) {
row.addText(createDelay(route))
row.setImage(NavigationUtils(carContext).createCarIcon(R.drawable.traffic_jam_48px))
row.setImage(createCarIcon(carContext = carContext, R.drawable.traffic_jam_48px))
}
return row.build()
}

View File

@@ -1,13 +1,63 @@
package com.kouros.navigation.car.screen
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import androidx.annotation.DrawableRes
import androidx.car.app.CarContext
import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Row
import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.IconCompat
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY
import com.kouros.navigation.data.ViewStyle
fun buildRowForTemplate(carContext: CarContext, title: Int, resource: Int): Row {
return Row.Builder()
.setTitle(carContext.getString(title))
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
resource
)
)
.build()
)
.build()
}
fun createNumberIcon(category: String, number: String): IconCompat {
val size = 24
val bitmap = createBitmap(size, size)
val canvas = Canvas(bitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textSize = size * 0.7f
textAlign = Paint.Align.CENTER
isFakeBoldText = true
}
val xPos = size / 2f
val yPos = (size / 2f) - ((paint.descent() + paint.ascent()) / 2f)
val color = when (category) {
CHARGING_STATION -> Color.GREEN
FUEL_STATION -> Color.BLUE
PHARMACY -> Color.RED
else -> Color.WHITE
}
paint.color = color
canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
paint.color = Color.WHITE
canvas.drawText(number, xPos, yPos, paint)
return IconCompat.createWithBitmap(bitmap)
}
fun createActionStrip(executeAction: () -> Action): ActionStrip {
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder()
@@ -17,7 +67,7 @@ fun createActionStrip(executeAction: () -> Action): ActionStrip {
return actionStripBuilder.build()
}
fun createActionStripBuilder(action1: () -> Action, action2: () -> Action): ActionStrip.Builder {
fun createActionStripBuilder(action1: () -> Action, action2: () -> Action): ActionStrip.Builder {
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder()
actionStripBuilder.addAction(
action1()
@@ -31,7 +81,12 @@ fun createActionStrip(executeAction: () -> Action): ActionStrip {
/**
* Creates an ActionStrip builder for map-related actions like zoom and pan.
*/
fun mapActionStrip(viewStyle: ViewStyle, zoomPlus: () -> Action, zoomMinus: () -> Action , panAction: () -> Action): ActionStrip {
fun mapActionStrip(
viewStyle: ViewStyle,
zoomPlus: () -> Action,
zoomMinus: () -> Action,
panAction: () -> Action
): ActionStrip {
val actionStripBuilder = ActionStrip.Builder()
.addAction(zoomPlus())
.addAction(zoomMinus())
@@ -47,7 +102,12 @@ fun mapActionStrip(viewStyle: ViewStyle, zoomPlus: () -> Action, zoomMinus: () -
/**
* Creates an action to do something.
*/
fun createAction(carContext: CarContext, @DrawableRes iconRes: Int, flag: Int = FLAG_DEFAULT, onClickAction: () -> Unit): Action {
fun createAction(
carContext: CarContext,
@DrawableRes iconRes: Int,
flag: Int = FLAG_DEFAULT,
onClickAction: () -> Unit
): Action {
return Action.Builder()
.setIcon(createCarIcon(carContext, iconRes))
.setFlags(flag)
@@ -60,3 +120,6 @@ fun createAction(carContext: CarContext, @DrawableRes iconRes: Int, flag: Int =
fun createCarIcon(carContext: CarContext, @DrawableRes iconRes: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
}

View File

@@ -14,12 +14,12 @@ import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants.CATEGORIES
import com.kouros.navigation.data.Constants.FAVORITES
import com.kouros.navigation.data.Constants.RECENT
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.NavigationViewModel

View File

@@ -13,7 +13,7 @@ import androidx.car.app.model.Template
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.screen.buildRowForTemplate
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -40,19 +40,22 @@ class AudioSettings(
val radioList =
ItemList.Builder()
.addItem(
NavigationUtils(carContext).buildRowForTemplate(
buildRowForTemplate(
carContext,
R.string.muted,
R.drawable.volume_off_24px
)
)
.addItem(
NavigationUtils(carContext).buildRowForTemplate(
buildRowForTemplate(
carContext,
R.string.unmuted,
R.drawable.volume_up_24px,
)
)
.addItem(
NavigationUtils(carContext).buildRowForTemplate(
buildRowForTemplate(
carContext,
R.string.alerts_only,
R.drawable.warning_24px,
)

View File

@@ -0,0 +1,77 @@
package com.kouros.navigation.car.screen.settings
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.Header
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.SectionedItemList
import androidx.car.app.model.Template
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.screen.buildRowForTemplate
import com.kouros.navigation.data.EngineType
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
class CarSettings(
private val carContext: CarContext,
private var navigationViewModel: NavigationViewModel
) :
Screen(carContext) {
private var engineType = EngineType.COMBUSTION.ordinal
val settingsViewModel = getSettingsViewModel(carContext)
init {
lifecycleScope.launch {
settingsViewModel.engineType.first()
}
}
override fun onGetTemplate(): Template {
engineType = settingsViewModel.engineType.value
val templateBuilder = ListTemplate.Builder()
val radioList =
ItemList.Builder()
.addItem(
buildRowForTemplate(carContext,
R.string.combustion,
R.drawable.ev_station_24px
)
)
.addItem(
buildRowForTemplate(carContext,
R.string.electric,
R.drawable.electric_car_24px
)
)
.setOnSelectedListener { index: Int ->
this.onSelected(index)
}
.setSelectedIndex(engineType)
.build()
return templateBuilder
.addSectionedList(
SectionedItemList.create(
radioList,
carContext.getString(R.string.engine_type)
)
)
.setHeader(
Header.Builder()
.setTitle(carContext.getString(R.string.car_settings))
.setStartHeaderAction(Action.BACK)
.build()
)
.build()
}
private fun onSelected(index: Int) {
settingsViewModel.onEngineTypeChanged(index)
}
}

View File

@@ -11,7 +11,7 @@ import androidx.car.app.model.SectionedItemList
import androidx.car.app.model.Template
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.screen.buildRowForTemplate
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -34,19 +34,19 @@ class DarkModeSettings(private val carContext: CarContext) : Screen(carContext)
val radioList =
ItemList.Builder()
.addItem(
NavigationUtils(carContext).buildRowForTemplate(
buildRowForTemplate(carContext,
R.string.off_action_title,
R.drawable.light_mode_24px
)
)
.addItem(
NavigationUtils(carContext).buildRowForTemplate(
buildRowForTemplate(carContext,
R.string.on_action_title,
R.drawable.dark_mode_24px
)
)
.addItem(
NavigationUtils(carContext).buildRowForTemplate(
buildRowForTemplate(carContext,
R.string.use_car_settings,
R.drawable.directions_car_24px
)

View File

@@ -13,7 +13,7 @@ import androidx.car.app.model.Toggle
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.screen.createCarIcon
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.flow.first
@@ -62,7 +62,7 @@ class NavigationSettings(
buildRowForTemplate(
R.string.avoid_highways_row_title,
highwayToggle,
NavigationUtils(carContext).createCarIcon(R.drawable.baseline_add_road_24)
createCarIcon(carContext, R.drawable.baseline_add_road_24)
)
)
@@ -72,7 +72,7 @@ class NavigationSettings(
settingsViewModel.onAvoidTollway(checked)
tollWayToggleState = !tollWayToggleState
}.setChecked(tollWayToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle, NavigationUtils(carContext).createCarIcon(R.drawable.baseline_toll_24)))
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle, createCarIcon(carContext,R.drawable.baseline_toll_24)))
// Ferry
val ferryToggle: Toggle =
@@ -80,7 +80,7 @@ class NavigationSettings(
settingsViewModel.onAvoidFerry(checked)
ferryToggleState = !ferryToggleState
}.setChecked(ferryToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.avoid_ferries, ferryToggle, NavigationUtils(carContext).createCarIcon(R.drawable.baseline_directions_boat_filled_24)))
listBuilder.addItem(buildRowForTemplate(R.string.avoid_ferries, ferryToggle, createCarIcon(carContext, R.drawable.baseline_directions_boat_filled_24)))
// CarLocation
val carLocationToggle: Toggle =
@@ -93,7 +93,7 @@ class NavigationSettings(
buildRowForTemplate(
R.string.use_car_location,
carLocationToggle,
NavigationUtils(carContext).createCarIcon(R.drawable.ic_place_white_24dp)
createCarIcon(carContext,R.drawable.ic_place_white_24dp)
)
)

View File

@@ -85,7 +85,7 @@ class SettingsScreen(
)
)
// Navigation --------------
// Drive settings --------------
listBuilder = ItemList.Builder()
listBuilder.addItem(
buildRowForTemplate(
@@ -94,10 +94,17 @@ class SettingsScreen(
)
)
listBuilder.addItem(
buildRowForTemplate(
CarSettings(carContext, navigationViewModel),
R.string.car_settings
)
)
templateBuilder.addSectionedList(
SectionedItemList.create(
listBuilder.build(),
carContext.getString(R.string.navigation_settings)
carContext.getString(R.string.drive_settings)
)
)
@@ -109,6 +116,8 @@ class SettingsScreen(
.setStartHeaderAction(Action.BACK)
.build())
.build()
}
private fun getTitle(): String {