Launcher Icons, NotificationService
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB |
@@ -137,7 +137,7 @@ class CarSensorManager(
|
||||
carCompassListener
|
||||
)
|
||||
carSensors.addCarHardwareLocationListener(
|
||||
CarSensors.UPDATE_RATE_FASTEST,
|
||||
CarSensors.UPDATE_RATE_NORMAL,
|
||||
carContext.mainExecutor,
|
||||
carLocationListener
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user