Categories
This commit is contained in:
@@ -55,7 +55,7 @@ class RouteModelTest {
|
||||
fun checkRoute() {
|
||||
assertEquals(true, routeModel.isNavigating())
|
||||
assertEquals(routeModel.curRoute.summary.distance, 11116.0, 10.0)
|
||||
assertEquals(routeModel.curRoute.summary.duration, 1148.0, 10.0)
|
||||
assertEquals(routeModel.curRoute.summary.duration, 1483.0, 10.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -66,7 +66,7 @@ class RouteModelTest {
|
||||
val stepData = routeModel.currentStep()
|
||||
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
|
||||
assertEquals(stepData.instruction, "Silcherstraße")
|
||||
assertEquals(stepData.leftStepDistance, 30.0, 1.0)
|
||||
assertEquals(stepData.leftStepDistance, 25.0, 1.0)
|
||||
val nextStepData = routeModel.nextStep()
|
||||
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
|
||||
assertEquals(nextStepData.instruction, "Schmalkaldener Straße")
|
||||
@@ -114,7 +114,7 @@ class RouteModelTest {
|
||||
val stepData = routeModel.currentStep()
|
||||
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT)
|
||||
assertEquals(stepData.instruction, "Schenkendorfstraße")
|
||||
assertEquals(stepData.leftStepDistance, 170.0, 1.0)
|
||||
assertEquals(stepData.leftStepDistance, 170.0, 10.0)
|
||||
assertEquals(stepData.lane.size, 4)
|
||||
assertEquals(stepData.lane.first().valid, true)
|
||||
assertEquals(stepData.lane.last().valid, false)
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.kouros.navigation.car.screen.RequestPermissionScreen
|
||||
import com.kouros.navigation.car.screen.SearchScreen
|
||||
import com.kouros.navigation.data.Constants.AUTOMOTIVE_CAR_SPEED_PERMISSION
|
||||
import com.kouros.navigation.data.Constants.GMS_CAR_SPEED_PERMISSION
|
||||
import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE
|
||||
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
|
||||
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
|
||||
import com.kouros.navigation.data.Constants.TAG
|
||||
@@ -40,6 +41,7 @@ import com.kouros.navigation.model.NavigationViewModel
|
||||
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
||||
import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
||||
import com.kouros.navigation.utils.getSettingsRepository
|
||||
import com.kouros.navigation.utils.location
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -77,6 +79,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
var autoDriveEnabled = false
|
||||
|
||||
val simulation = Simulation()
|
||||
|
||||
/**
|
||||
* Lifecycle observer for managing session lifecycle events.
|
||||
* Cleans up resources when the session is destroyed.
|
||||
@@ -205,8 +208,13 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
navigationManager.setNavigationManagerCallback(object : NavigationManagerCallback {
|
||||
override fun onAutoDriveEnabled() {
|
||||
// Called when the app should simulate navigation (e.g., for testing)
|
||||
// Implement your simulation logic here
|
||||
deviceLocationManager.stopLocationUpdates()
|
||||
autoDriveEnabled = true
|
||||
simulation.startSimulation(
|
||||
routeModel, lifecycle.coroutineScope
|
||||
) { location ->
|
||||
updateLocation(location)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopNavigation() {
|
||||
@@ -214,6 +222,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
// Stop turn-by-turn logic and clean up
|
||||
routeModel.stopNavigation()
|
||||
autoDriveEnabled = false
|
||||
deviceLocationManager.startLocationUpdates()
|
||||
}
|
||||
})
|
||||
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
|
||||
@@ -240,6 +249,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
textToSpeechManager = TextToSpeechManager(carContext)
|
||||
val repository = getSettingsRepository(carContext)
|
||||
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
|
||||
@@ -342,9 +352,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
* Handles route snapping, deviation detection for rerouting, and map updates.
|
||||
*/
|
||||
fun updateLocation(location: Location) {
|
||||
if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION ) {
|
||||
surfaceRenderer.updateCarSpeed(location.speed)
|
||||
}
|
||||
if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) {
|
||||
surfaceRenderer.updateCarSpeed(location.speed)
|
||||
}
|
||||
updateBearing(location)
|
||||
if (routeModel.isNavigating()) {
|
||||
handleNavigationLocation(location)
|
||||
@@ -410,7 +420,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
navigationManager.navigationStarted()
|
||||
if (autoDriveEnabled) {
|
||||
simulation.startSimulation(
|
||||
routeModel, lifecycle.coroutineScope
|
||||
routeModel, lifecycle.coroutineScope
|
||||
) { location ->
|
||||
updateLocation(location)
|
||||
}
|
||||
@@ -418,7 +428,6 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
}
|
||||
|
||||
override fun updateTrip(trip: Trip) {
|
||||
Log.d("Trip", trip.toString())
|
||||
navigationManager.updateTrip(trip)
|
||||
}
|
||||
|
||||
@@ -429,10 +438,8 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
private fun handleGuidanceAudio() {
|
||||
val currentStep = routeModel.route.currentStep()
|
||||
val stepData = routeModel.currentStep()
|
||||
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < 50) {
|
||||
if (textToSpeechManager.initialized) {
|
||||
textToSpeechManager.speak(stepData.message)
|
||||
}
|
||||
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) {
|
||||
textToSpeechManager.speak(stepData.message)
|
||||
lastStepIndex = currentStep.index
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class SurfaceRenderer(
|
||||
// Current camera position state for the map
|
||||
private val cameraPosition = MutableLiveData(
|
||||
CameraPosition(
|
||||
zoom = 15.0,
|
||||
zoom = 16.0,
|
||||
target = Position(latitude = homeVogelhart.latitude, longitude = homeVogelhart.longitude)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,61 +1,81 @@
|
||||
package com.kouros.navigation.car
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioFocusRequest
|
||||
import android.media.AudioManager
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.speech.tts.UtteranceProgressListener
|
||||
import android.util.Log
|
||||
|
||||
|
||||
import androidx.car.app.CarContext
|
||||
import java.util.Locale
|
||||
|
||||
class TextToSpeechManager(private val carContext: CarContext) {
|
||||
class TextToSpeechManager(private val carContext: Context) {
|
||||
|
||||
var textToSpeech: TextToSpeech
|
||||
private var textToSpeech: TextToSpeech? = null
|
||||
@Volatile private var initialized = false
|
||||
|
||||
var initialized = false
|
||||
private val audioManager: AudioManager by lazy {
|
||||
carContext.getSystemService(AudioManager::class.java)!!
|
||||
}
|
||||
|
||||
private val audioAttributes = AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
|
||||
.build()
|
||||
|
||||
private val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
|
||||
.setAudioAttributes(audioAttributes)
|
||||
.setOnAudioFocusChangeListener { /* Handle focus changes if needed */ }
|
||||
.build()
|
||||
|
||||
init {
|
||||
textToSpeech = TextToSpeech(carContext) { status ->
|
||||
if (status == TextToSpeech.SUCCESS) {
|
||||
Log.d("TTS", "Initialization Success")
|
||||
val audioAttributes =
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
|
||||
.build()
|
||||
val request =
|
||||
AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
|
||||
.setAudioAttributes(audioAttributes)
|
||||
.build()
|
||||
val audioManager: AudioManager =
|
||||
carContext.getSystemService<AudioManager?>(AudioManager::class.java)!!
|
||||
// Requesting the audio focus.
|
||||
if (audioManager.requestAudioFocus(request) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
textToSpeech.setAudioAttributes(audioAttributes)
|
||||
textToSpeech?.apply {
|
||||
setAudioAttributes(audioAttributes)
|
||||
setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
||||
override fun onStart(utteranceId: String?) {}
|
||||
|
||||
override fun onDone(utteranceId: String?) {
|
||||
// Release focus ONLY after speech is finished
|
||||
audioManager.abandonAudioFocusRequest(focusRequest)
|
||||
}
|
||||
|
||||
override fun onError(utteranceId: String) {
|
||||
audioManager.abandonAudioFocusRequest(focusRequest)
|
||||
}
|
||||
})
|
||||
}
|
||||
initialized = true
|
||||
Log.d("TTS", "Initialization Success")
|
||||
} else {
|
||||
Log.d("TTS", "Initialization Failed")
|
||||
Log.e("TTS", "Initialization Failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun speak(text: String) {
|
||||
try {
|
||||
val cs: CharSequence = text
|
||||
textToSpeech.speak(cs, TextToSpeech.QUEUE_FLUSH, null, "1233455")
|
||||
} catch (e: Throwable) {
|
||||
Log.d("TTS", "speak error", e)
|
||||
if (!initialized) {
|
||||
Log.w("TTS", "Ignore speak: Not initialized yet")
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Request focus
|
||||
val result = audioManager.requestAudioFocus(focusRequest)
|
||||
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
// 2. Speak with a unique ID to trigger the listener
|
||||
val utteranceId = System.currentTimeMillis().toString()
|
||||
textToSpeech?.speak(text, TextToSpeech.QUEUE_FLUSH, null, utteranceId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up manager.
|
||||
* Should be called when the session is destroyed.
|
||||
*/
|
||||
fun cleanup() {
|
||||
if (initialized) {
|
||||
textToSpeech.shutdown()
|
||||
textToSpeech?.stop()
|
||||
textToSpeech?.shutdown()
|
||||
initialized = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,9 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.scale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -22,6 +24,7 @@ import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.drawText
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.kouros.data.R
|
||||
@@ -210,10 +213,10 @@ fun RouteLayerPoint(routeData: String?) {
|
||||
fun trafficColor(key: String): Expression<ColorValue> {
|
||||
when (key) {
|
||||
"queuing" -> return const(Color(0xFFC46E53))
|
||||
"stationary" -> return const(Color(0xFFFF0000))
|
||||
"slow" -> return const(Color(0xFFC43E3E))
|
||||
"stationary" -> return const(Color(0xFF910A0A))
|
||||
"heavy" -> return const(Color(0xFF6B0404))
|
||||
"slow" -> return const(Color(0xFFBD2525))
|
||||
"roadworks" -> return const(Color(0xFF725A0F))
|
||||
"roadworks" -> return const(Color(0xFF443506))
|
||||
}
|
||||
return const(Color.Blue)
|
||||
}
|
||||
@@ -222,10 +225,10 @@ fun trafficColor(key: String): Expression<ColorValue> {
|
||||
fun AmenityLayer(routeData: String?) {
|
||||
if (!routeData.isNullOrEmpty()) {
|
||||
var color = const(Color.Red)
|
||||
var img = image(painterResource(R.drawable.local_pharmacy_48px), drawAsSdf = true)
|
||||
var img = image(painterResource(R.drawable.local_pharmacy_24px), drawAsSdf = true)
|
||||
if (routeData.contains(Constants.CHARGING_STATION)) {
|
||||
color = const(Color(0xFF054603))
|
||||
img = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true)
|
||||
img = image(painterResource(R.drawable.ev_station_24px), drawAsSdf = true)
|
||||
} else if (routeData.contains(Constants.FUEL_STATION)) {
|
||||
color = const(Color.Blue)
|
||||
img = image(painterResource(R.drawable.local_gas_station_24), drawAsSdf = true)
|
||||
@@ -321,10 +324,11 @@ fun NavigationImage(
|
||||
val street = streetName.toString()
|
||||
val styleStreet = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
color = if (darkMode == 1) Color.Yellow else Color.Red,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (darkMode == 1) Color.White else navigationColor,
|
||||
)
|
||||
val textLayoutStreet = remember(street) {
|
||||
textMeasurerStreet.measure(street, styleStreet)
|
||||
textMeasurerStreet.measure(street, styleStreet, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(padding)) {
|
||||
@@ -346,16 +350,26 @@ fun NavigationImage(
|
||||
)
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.size(imageSize.dp + 100.dp, imageSize.dp + 80.dp)
|
||||
.size((width / 5).dp, (height/4).dp)
|
||||
) {
|
||||
if (!streetName.isNullOrEmpty()) {
|
||||
drawRoundRect(
|
||||
topLeft = Offset(
|
||||
x = center.x - textLayoutStreet.size.width / 2 ,
|
||||
y = center.y + textLayoutStreet.size.height,
|
||||
),
|
||||
color = if (darkMode == 1) navigationColor else Color.White,
|
||||
cornerRadius = CornerRadius(x = 10f, y = 10f),
|
||||
)
|
||||
drawText(
|
||||
textMeasurer = textMeasurerStreet,
|
||||
text = streetName,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = styleStreet,
|
||||
topLeft = Offset(
|
||||
x = center.x - textLayoutStreet.size.width / 2,
|
||||
y = center.y + textLayoutStreet.size.height,
|
||||
y = center.y + textLayoutStreet.size.height + 10,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
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
|
||||
@@ -16,7 +19,12 @@ 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
|
||||
@@ -66,4 +74,31 @@ class NavigationUtils(private var carContext: CarContext) {
|
||||
)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class Simulation {
|
||||
// Update your app's state as if a real GPS update occurred
|
||||
updateLocation(fakeLocation)
|
||||
// Wait before moving to the next point (e.g., every 2 seconds)
|
||||
delay(500)
|
||||
delay(1000)
|
||||
lastLocation = fakeLocation
|
||||
}
|
||||
routeModel.stopNavigation()
|
||||
|
||||
@@ -18,19 +18,32 @@ 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.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,
|
||||
private val surfaceRenderer: SurfaceRenderer,
|
||||
private val navigationViewModel: NavigationViewModel,
|
||||
) : Screen(carContext) {
|
||||
) : Screen(carContext), CategoryObserverCallback {
|
||||
|
||||
private val categoryObserver = CategoryObserver(this)
|
||||
|
||||
private var category = ""
|
||||
var categories: List<Category> = listOf(
|
||||
Category(id = FUEL_STATION, name = carContext.getString(R.string.fuel_station)),
|
||||
Category(id = PHARMACY, name = carContext.getString(R.string.pharmacy)),
|
||||
Category(id = CHARGING_STATION, name = carContext.getString(R.string.charging_station))
|
||||
)
|
||||
|
||||
init {
|
||||
navigationViewModel.elements.observe(this, categoryObserver)
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
val itemListBuilder = ItemList.Builder()
|
||||
.setNoItemsMessage("No categories to show")
|
||||
@@ -38,22 +51,10 @@ class CategoriesScreen(
|
||||
itemListBuilder.addItem(
|
||||
Row.Builder()
|
||||
.setTitle(it.name)
|
||||
.setImage(carIcon(carContext,it.id))
|
||||
.setImage(carIcon(carContext,it.id, -1))
|
||||
.setOnClickListener {
|
||||
screenManager
|
||||
.pushForResult(
|
||||
CategoryScreen(
|
||||
carContext,
|
||||
surfaceRenderer,
|
||||
it.id,
|
||||
navigationViewModel
|
||||
)
|
||||
) { obj: Any? ->
|
||||
if (obj != null) {
|
||||
setResult(obj)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
category = it.id
|
||||
navigationViewModel.getAmenities(it.id, surfaceRenderer.lastLocation)
|
||||
}
|
||||
.setBrowsable(true)
|
||||
.build()
|
||||
@@ -72,20 +73,49 @@ class CategoriesScreen(
|
||||
.setSingleList(itemListBuilder.build())
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onCategoryElementsReady(
|
||||
elements: List<Elements>,
|
||||
centerLat: Double,
|
||||
centerLon: Double,
|
||||
coordinates: List<List<Double>>
|
||||
) {
|
||||
val loc = location(centerLon, centerLat)
|
||||
val route = createPointCollection(coordinates, category)
|
||||
surfaceRenderer.setCategories(loc, route)
|
||||
screenManager
|
||||
.pushForResult(
|
||||
CategoryScreen(
|
||||
carContext,
|
||||
surfaceRenderer,
|
||||
category,
|
||||
navigationViewModel,
|
||||
elements
|
||||
)
|
||||
) { obj: Any? ->
|
||||
if (obj != null) {
|
||||
setResult(obj)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidateScreen() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun carIcon(context: CarContext, id: String): CarIcon {
|
||||
val resId = when (id) {
|
||||
FUEL_STATION -> R.drawable.local_gas_station_24
|
||||
PHARMACY -> R.drawable.local_pharmacy_48px
|
||||
CHARGING_STATION -> R.drawable.ev_station_48px
|
||||
else -> {}
|
||||
fun carIcon(context: CarContext, category: String, index: Int): CarIcon {
|
||||
if (index == -1) {
|
||||
val resId = when (category) {
|
||||
CHARGING_STATION -> R.drawable.ev_station_24px
|
||||
FUEL_STATION -> R.drawable.local_gas_station_24
|
||||
PHARMACY -> R.drawable.local_pharmacy_24px
|
||||
else -> R.drawable.ic_place_white_24dp
|
||||
}
|
||||
return CarIcon.Builder(IconCompat.createWithResource(context, resId)).build()
|
||||
} else {
|
||||
return CarIcon.Builder(NavigationUtils(context).createNumberIcon(category, index.toString())).build()
|
||||
}
|
||||
return CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
context,
|
||||
resId as Int
|
||||
)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,16 @@ import androidx.car.app.model.Row
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.car.app.navigation.model.MapController
|
||||
import androidx.car.app.navigation.model.MapWithContentTemplate
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.car.app.versioning.CarAppApiLevels
|
||||
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
|
||||
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.Place
|
||||
import com.kouros.navigation.data.overpass.Elements
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
@@ -32,57 +37,39 @@ class CategoryScreen(
|
||||
private val surfaceRenderer: SurfaceRenderer,
|
||||
private val category: String,
|
||||
private val navigationViewModel: NavigationViewModel,
|
||||
private var elements: List<Elements>,
|
||||
) : Screen(carContext) {
|
||||
|
||||
var elements = listOf<Elements>()
|
||||
|
||||
val observer = Observer<List<Elements>> { newElements ->
|
||||
elements = newElements
|
||||
val coordinates = mutableListOf<List<Double>>()
|
||||
val loc = location(0.0, 0.0)
|
||||
elements.forEach {
|
||||
if (loc.latitude == 0.0) {
|
||||
loc.longitude = it.lon
|
||||
loc.latitude = it.lat
|
||||
}
|
||||
coordinates.add(listOf(it.lon, it.lat))
|
||||
}
|
||||
if (elements.isNotEmpty()) {
|
||||
val route = createPointCollection(coordinates, category)
|
||||
surfaceRenderer.setCategories(loc, route)
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
navigationViewModel.elements.observe(this, observer)
|
||||
navigationViewModel.getAmenities(category, surfaceRenderer.lastLocation)
|
||||
}
|
||||
val maxListItems: Int = 30
|
||||
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
val listBuilder = ItemList.Builder()
|
||||
var index = 0
|
||||
val listLimit = min(
|
||||
50,
|
||||
carContext.getCarService(ConstraintManager::class.java)
|
||||
.getContentLimit(
|
||||
ConstraintManager.CONTENT_LIMIT_TYPE_LIST
|
||||
)
|
||||
)
|
||||
elements.forEach {
|
||||
if (index++ < listLimit) {
|
||||
if (it.tags.operator != null) {
|
||||
listBuilder.addItem(
|
||||
createItem(it, category)
|
||||
|
||||
// Some hosts may allow more items in the list than others, so create more.
|
||||
if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
|
||||
val listLimit = min(
|
||||
maxListItems,
|
||||
carContext.getCarService(ConstraintManager::class.java)
|
||||
.getContentLimit(
|
||||
ConstraintManager.CONTENT_LIMIT_TYPE_LIST
|
||||
)
|
||||
)
|
||||
elements.forEach {
|
||||
if (it.tags.operator != null) {
|
||||
if (index++ < listLimit) {
|
||||
listBuilder.addItem(
|
||||
createItem(it, category, index)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val header = Header.Builder()
|
||||
.setStartHeaderAction(Action.BACK)
|
||||
.setTitle(carContext.getString(R.string.charging_station))
|
||||
.setTitle(getTitle(carContext, category))
|
||||
.build()
|
||||
val builder = MapWithContentTemplate.Builder()
|
||||
.setContentTemplate(
|
||||
@@ -99,7 +86,17 @@ class CategoryScreen(
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun createItem(it: Elements, category: String): Row {
|
||||
private fun getTitle(carContext: CarContext, category: String): String {
|
||||
val resId = when (category) {
|
||||
CHARGING_STATION -> R.string.charging_station
|
||||
FUEL_STATION -> R.string.fuel_station
|
||||
PHARMACY -> R.string.pharmacy
|
||||
else -> R.string.no_places
|
||||
}
|
||||
return carContext.getString(resId)
|
||||
}
|
||||
|
||||
private fun createItem(it: Elements, category: String, index: Int): Row {
|
||||
var name = ""
|
||||
if (it.tags.name != null) {
|
||||
name = it.tags.name.toString()
|
||||
@@ -113,7 +110,7 @@ class CategoryScreen(
|
||||
surfaceRenderer.setCategoryLocation(location, category)
|
||||
}
|
||||
.setTitle(name)
|
||||
.setImage(carIcon(carContext, category))
|
||||
.setImage(carIcon(carContext, category, index))
|
||||
if (it.distance < 1000) {
|
||||
row.addText("${(it.distance).toInt()} m")
|
||||
} else {
|
||||
@@ -127,28 +124,29 @@ class CategoryScreen(
|
||||
val navigationUtils = NavigationUtils(carContext)
|
||||
row.addAction(
|
||||
Action.Builder()
|
||||
.setOnClickListener {
|
||||
navigationViewModel.loadRoute(
|
||||
carContext,
|
||||
currentLocation = surfaceRenderer.lastLocation,
|
||||
location(it.lon!!, it.lat!!),
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
setResult(
|
||||
Place(
|
||||
name = name,
|
||||
category = Constants.CHARGING_STATION,
|
||||
latitude = it.lat!!,
|
||||
longitude = it.lon!!
|
||||
.setOnClickListener {
|
||||
navigationViewModel.loadRoute(
|
||||
carContext,
|
||||
currentLocation = surfaceRenderer.lastLocation,
|
||||
location(it.lon, it.lat),
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
)
|
||||
finish()
|
||||
}
|
||||
.setIcon(navigationUtils.createCarIcon(R.drawable.navigation_48px))
|
||||
.build())
|
||||
setResult(
|
||||
Place(
|
||||
name = name,
|
||||
category = Constants.CHARGING_STATION,
|
||||
latitude = it.lat,
|
||||
longitude = it.lon
|
||||
)
|
||||
)
|
||||
finish()
|
||||
}
|
||||
.setIcon(navigationUtils.createCarIcon(R.drawable.navigation_48px))
|
||||
.build())
|
||||
return row.build()
|
||||
}
|
||||
|
||||
|
||||
private fun carText(sText: String): CarText {
|
||||
val secondText =
|
||||
CarText.Builder(
|
||||
|
||||
@@ -40,8 +40,6 @@ import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.data.overpass.Elements
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
import com.kouros.navigation.model.SettingsViewModel
|
||||
import com.kouros.navigation.repository.SettingsRepository
|
||||
import com.kouros.navigation.utils.GeoUtils
|
||||
import com.kouros.navigation.utils.formattedDistance
|
||||
import com.kouros.navigation.utils.getSettingsRepository
|
||||
@@ -74,7 +72,7 @@ class NavigationScreen(
|
||||
|
||||
/** Starts navigation. */
|
||||
fun startNavigation()
|
||||
|
||||
|
||||
/** Updates trip information. */
|
||||
fun updateTrip(trip: Trip)
|
||||
}
|
||||
@@ -102,8 +100,10 @@ class NavigationScreen(
|
||||
lifecycleScope.launch {
|
||||
settingsViewModel.routingEngine.first()
|
||||
settingsViewModel.recentPlaces.first()
|
||||
distanceMode = repository.distanceModeFlow.first()
|
||||
}
|
||||
repository.distanceModeFlow.asLiveData().observe(this, Observer {
|
||||
distanceMode = it
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,7 +112,7 @@ class NavigationScreen(
|
||||
*/
|
||||
override fun onRouteReceived(route: String) {
|
||||
if (route.isNotEmpty()) {
|
||||
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
|
||||
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
|
||||
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
|
||||
navigationType = NavigationType.NAVIGATION
|
||||
routeModel.startNavigation(route)
|
||||
@@ -187,9 +187,7 @@ class NavigationScreen(
|
||||
* Returns the appropriate template based on the current navigation state.
|
||||
*/
|
||||
override fun onGetTemplate(): Template {
|
||||
repository.distanceModeFlow.asLiveData().observe(this, Observer {
|
||||
distanceMode = it
|
||||
})
|
||||
|
||||
val actionStripBuilder = createActionStripBuilder()
|
||||
return when (navigationType) {
|
||||
NavigationType.NAVIGATION -> navigationTemplate(actionStripBuilder)
|
||||
@@ -330,8 +328,9 @@ class NavigationScreen(
|
||||
* Builds and returns RoutingInfo based on the current step and distance.
|
||||
*/
|
||||
fun getRoutingInfo(): RoutingInfo {
|
||||
val distance = formattedDistance(distanceMode, routeModel.routeCalculator.leftStepDistance())
|
||||
val routingInfo = RoutingInfo.Builder()
|
||||
val distance =
|
||||
formattedDistance(distanceMode, routeModel.routeCalculator.leftStepDistance())
|
||||
val routingInfo = RoutingInfo.Builder()
|
||||
.setCurrentStep(
|
||||
routeModel.currentStep(carContext = carContext),
|
||||
Distance.create(distance.first, distance.second)
|
||||
@@ -460,7 +459,7 @@ class NavigationScreen(
|
||||
* Creates an action to start the settings screen.
|
||||
*/
|
||||
private fun settingsAction(): Action {
|
||||
return Action.Builder()
|
||||
return Action.Builder()
|
||||
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px))
|
||||
.setOnClickListener {
|
||||
screenManager.push(SettingsScreen(carContext, navigationViewModel))
|
||||
@@ -628,7 +627,7 @@ class NavigationScreen(
|
||||
}
|
||||
updateSpeedCamera(location)
|
||||
with(routeModel) {
|
||||
updateLocation( location, navigationViewModel)
|
||||
updateLocation(location, navigationViewModel)
|
||||
checkArrival()
|
||||
}
|
||||
invalidate()
|
||||
@@ -658,14 +657,19 @@ class NavigationScreen(
|
||||
* This includes destination name, address, travel estimate, and loading status.
|
||||
*/
|
||||
private fun updateTrip() {
|
||||
val tripBuilder = Trip.Builder()
|
||||
val destination = Destination.Builder()
|
||||
.setName(routeModel.navState.destination.name ?: "")
|
||||
.setAddress(routeModel.navState.destination.street ?: "")
|
||||
.build()
|
||||
tripBuilder.addDestination(destination, routeModel.travelEstimate(carContext, distanceMode))
|
||||
tripBuilder.setLoading(false)
|
||||
listener.updateTrip(tripBuilder.build())
|
||||
if (routeModel.isNavigating()) {
|
||||
val tripBuilder = Trip.Builder()
|
||||
val destination = Destination.Builder()
|
||||
.setName(routeModel.navState.destination.name ?: "")
|
||||
.setAddress(routeModel.navState.destination.street ?: "")
|
||||
.build()
|
||||
tripBuilder.addDestination(
|
||||
destination,
|
||||
routeModel.travelEstimate(carContext, distanceMode)
|
||||
)
|
||||
tripBuilder.setLoading(false)
|
||||
listener.updateTrip(tripBuilder.build())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -142,22 +142,15 @@ class SearchScreen(
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun doSearch(searchItemListBuilder: ItemList.Builder) {
|
||||
if (searchResult.size == 1) {
|
||||
navigateToPlace(searchResult.first())
|
||||
}
|
||||
searchResult.forEach {
|
||||
searchItemListBuilder.addItem(
|
||||
Row.Builder()
|
||||
.setTitle("${(it.distance/1000).toInt()} km ${it.displayName} ")
|
||||
.setOnClickListener {
|
||||
val place = Place(
|
||||
name = it.displayName,
|
||||
latitude = it.lat.toDouble(),
|
||||
longitude = it.lon.toDouble(),
|
||||
street = it.address.road,
|
||||
city = it.address.city,
|
||||
postalCode = it.address.postcode,
|
||||
distance = it.distance
|
||||
)
|
||||
setResult(place)
|
||||
finish()
|
||||
navigateToPlace(it)
|
||||
}
|
||||
.setBrowsable(false)
|
||||
.build()
|
||||
@@ -165,4 +158,18 @@ class SearchScreen(
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private fun navigateToPlace(result: SearchResult) {
|
||||
val place = Place(
|
||||
name = result.displayName,
|
||||
latitude = result.lat.toDouble(),
|
||||
longitude = result.lon.toDouble(),
|
||||
street = result.address.road,
|
||||
city = result.address.city,
|
||||
postalCode = result.address.postcode,
|
||||
distance = result.distance
|
||||
)
|
||||
setResult(place)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.kouros.navigation.car.screen.observers
|
||||
|
||||
import androidx.lifecycle.Observer
|
||||
import com.kouros.navigation.data.overpass.Elements
|
||||
|
||||
/**
|
||||
* Callback interface for category element updates in CategoryScreen.
|
||||
*/
|
||||
interface CategoryObserverCallback {
|
||||
/**
|
||||
* Called when category elements are ready to display.
|
||||
* @param elements the full list of elements
|
||||
* @param centerLat latitude of the first element (used to center the map)
|
||||
* @param centerLon longitude of the first element (used to center the map)
|
||||
* @param coordinates all element coordinates as [lon, lat] pairs
|
||||
*/
|
||||
fun onCategoryElementsReady(
|
||||
elements: List<Elements>,
|
||||
centerLat: Double,
|
||||
centerLon: Double,
|
||||
coordinates: List<List<Double>>
|
||||
)
|
||||
|
||||
/** Called to request UI invalidation after elements are updated */
|
||||
fun invalidateScreen()
|
||||
}
|
||||
|
||||
/**
|
||||
* Observer for POI/amenity element lists. Extracts coordinates and notifies the screen
|
||||
* when new data is ready to display.
|
||||
*/
|
||||
class CategoryObserver(
|
||||
private val callback: CategoryObserverCallback
|
||||
) : Observer<List<Elements>> {
|
||||
|
||||
override fun onChanged(value: List<Elements>) {
|
||||
if (value.isEmpty()) return
|
||||
|
||||
var centerLat = 0.0
|
||||
var centerLon = 0.0
|
||||
val coordinates = mutableListOf<List<Double>>()
|
||||
|
||||
value.forEach { element ->
|
||||
if (centerLat == 0.0) {
|
||||
centerLat = element.lat
|
||||
centerLon = element.lon
|
||||
}
|
||||
coordinates.add(listOf(element.lon, element.lat))
|
||||
}
|
||||
|
||||
callback.onCategoryElementsReady(value, centerLat, centerLon, coordinates)
|
||||
callback.invalidateScreen()
|
||||
}
|
||||
}
|
||||
@@ -83,5 +83,4 @@ class AudioSettings(
|
||||
private fun onSelected(index: Int) {
|
||||
settingsViewModel.onGuidanceAudioChanged(index)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.car.app.model.ListTemplate
|
||||
import androidx.car.app.model.Row
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.car.app.model.Toggle
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.car.screen.settings.DistanceSettings
|
||||
@@ -20,16 +21,19 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
|
||||
|
||||
private var buildingToggleState = false
|
||||
|
||||
private var showTraffic = false
|
||||
|
||||
val settingsViewModel = getSettingsViewModel(carContext)
|
||||
|
||||
init {
|
||||
lifecycleScope.launch {
|
||||
settingsViewModel.show3D.first()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
buildingToggleState = settingsViewModel.show3D.value
|
||||
settingsViewModel.traffic.asLiveData().observe(this) {
|
||||
showTraffic = it
|
||||
invalidate()
|
||||
}
|
||||
settingsViewModel.show3D.asLiveData().observe(this) {
|
||||
buildingToggleState = it
|
||||
invalidate()
|
||||
}
|
||||
val listBuilder = ItemList.Builder()
|
||||
val buildingToggle: Toggle =
|
||||
Toggle.Builder { checked: Boolean ->
|
||||
@@ -37,6 +41,14 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
|
||||
buildingToggleState = !buildingToggleState
|
||||
}.setChecked(buildingToggleState).build()
|
||||
listBuilder.addItem(buildRowForTemplate(R.string.threed_building, buildingToggle))
|
||||
|
||||
val trafficToggle: Toggle =
|
||||
Toggle.Builder { checked: Boolean ->
|
||||
settingsViewModel.onTraffic(checked)
|
||||
showTraffic = !showTraffic
|
||||
}.setChecked(showTraffic).build()
|
||||
listBuilder.addItem(buildRowForTemplate(R.string.traffic, trafficToggle))
|
||||
|
||||
listBuilder.addItem(
|
||||
buildRowForScreenTemplate(
|
||||
DarkModeSettings(carContext),
|
||||
|
||||
@@ -34,7 +34,7 @@ class DistanceSettings(private val carContext: CarContext) : Screen(carContext)
|
||||
ItemList.Builder()
|
||||
.addItem(
|
||||
buildRowForTemplate(
|
||||
R.string.automaticaly,
|
||||
R.string.automatically,
|
||||
)
|
||||
)
|
||||
.addItem(
|
||||
|
||||
@@ -28,6 +28,9 @@ class NavigationSettings(
|
||||
|
||||
private var tollWayToggleState = false
|
||||
|
||||
private var ferryToggleState = false
|
||||
|
||||
|
||||
private var carLocationToggleState = false
|
||||
|
||||
val settingsViewModel = getSettingsViewModel(carContext)
|
||||
@@ -36,6 +39,7 @@ class NavigationSettings(
|
||||
lifecycleScope.launch {
|
||||
settingsViewModel.avoidTollway.first()
|
||||
settingsViewModel.avoidMotorway.first()
|
||||
settingsViewModel.avoidFerry.first()
|
||||
settingsViewModel.carLocation.first()
|
||||
}
|
||||
}
|
||||
@@ -43,9 +47,12 @@ class NavigationSettings(
|
||||
override fun onGetTemplate(): Template {
|
||||
motorWayToggleState = settingsViewModel.avoidMotorway.value
|
||||
tollWayToggleState = settingsViewModel.avoidTollway.value
|
||||
ferryToggleState = settingsViewModel.avoidFerry.value
|
||||
carLocationToggleState = settingsViewModel.carLocation.value
|
||||
|
||||
val listBuilder = ItemList.Builder()
|
||||
|
||||
// Motorway
|
||||
val highwayToggle: Toggle =
|
||||
Toggle.Builder { checked: Boolean ->
|
||||
settingsViewModel.onAvoidMotorway(checked)
|
||||
@@ -66,6 +73,15 @@ class NavigationSettings(
|
||||
}.setChecked(tollWayToggleState).build()
|
||||
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle))
|
||||
|
||||
// Ferry
|
||||
val ferryToggle: Toggle =
|
||||
Toggle.Builder { checked: Boolean ->
|
||||
settingsViewModel.onAvoidFerry(checked)
|
||||
ferryToggleState = !ferryToggleState
|
||||
}.setChecked(ferryToggleState).build()
|
||||
listBuilder.addItem(buildRowForTemplate(R.string.avoid_ferries, ferryToggle))
|
||||
|
||||
// CarLocation
|
||||
val carLocationToggle: Toggle =
|
||||
Toggle.Builder { checked: Boolean ->
|
||||
settingsViewModel.onCarLocation(checked)
|
||||
|
||||
@@ -3,13 +3,23 @@ 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.CarIcon
|
||||
import androidx.car.app.model.Header
|
||||
import androidx.car.app.model.ItemList
|
||||
import androidx.car.app.model.ListTemplate
|
||||
import androidx.car.app.model.Row
|
||||
import androidx.car.app.model.SectionedItemList
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.car.app.model.Toggle
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
import com.kouros.navigation.utils.getSettingsViewModel
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** A screen demonstrating selectable lists. */
|
||||
class SettingsScreen(
|
||||
@@ -17,20 +27,66 @@ class SettingsScreen(
|
||||
private var navigationViewModel: NavigationViewModel,
|
||||
) : Screen(carContext) {
|
||||
|
||||
val settingsViewModel = getSettingsViewModel(carContext)
|
||||
|
||||
private var audioToggleState = false
|
||||
|
||||
init {
|
||||
lifecycleScope.launch {
|
||||
settingsViewModel.guidanceAudio.first()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
val listBuilder = ItemList.Builder()
|
||||
listBuilder.addItem(
|
||||
buildRowForTemplate(
|
||||
AudioSettings(carContext),
|
||||
R.string.audio_settings
|
||||
)
|
||||
)
|
||||
settingsViewModel.guidanceAudio.asLiveData().observe(this, Observer {
|
||||
audioToggleState = settingsViewModel.guidanceAudio.value == 1
|
||||
invalidate()
|
||||
})
|
||||
|
||||
val templateBuilder = ListTemplate.Builder()
|
||||
|
||||
val audioToggle: Toggle =
|
||||
Toggle.Builder { checked: Boolean ->
|
||||
if (checked) {
|
||||
settingsViewModel.onGuidanceAudioChanged(1)
|
||||
} else {
|
||||
settingsViewModel.onGuidanceAudioChanged(0)
|
||||
}
|
||||
audioToggleState = !audioToggleState
|
||||
}.setChecked(audioToggleState).build()
|
||||
|
||||
|
||||
|
||||
var listBuilder = ItemList.Builder()
|
||||
listBuilder.addItem(Row.Builder()
|
||||
.setTitle(getTitle())
|
||||
.setImage(getImage())
|
||||
.setToggle(audioToggle)
|
||||
.build())
|
||||
|
||||
listBuilder.addItem(
|
||||
buildRowForTemplate(
|
||||
DisplaySettings(carContext),
|
||||
R.string.display
|
||||
)
|
||||
)
|
||||
|
||||
listBuilder.addItem(
|
||||
buildRowForTemplate(
|
||||
AudioSettings(carContext),
|
||||
R.string.audio_settings
|
||||
)
|
||||
)
|
||||
|
||||
templateBuilder.addSectionedList(
|
||||
SectionedItemList.create(
|
||||
listBuilder.build(),
|
||||
carContext.getString(R.string.general)
|
||||
)
|
||||
)
|
||||
|
||||
// Navigation --------------
|
||||
listBuilder = ItemList.Builder()
|
||||
listBuilder.addItem(
|
||||
buildRowForTemplate(
|
||||
NavigationSettings(carContext, navigationViewModel),
|
||||
@@ -38,19 +94,41 @@ class SettingsScreen(
|
||||
)
|
||||
)
|
||||
|
||||
return ListTemplate.Builder()
|
||||
.setSingleList(listBuilder.build())
|
||||
.setHeader(
|
||||
Header.Builder()
|
||||
.setTitle(
|
||||
(carContext.getString(R.string.settings_action_title))
|
||||
)
|
||||
.setStartHeaderAction(Action.BACK)
|
||||
.build()
|
||||
templateBuilder.addSectionedList(
|
||||
SectionedItemList.create(
|
||||
listBuilder.build(),
|
||||
carContext.getString(R.string.navigation_settings)
|
||||
)
|
||||
)
|
||||
|
||||
return templateBuilder
|
||||
.setHeader( Header.Builder()
|
||||
.setTitle(
|
||||
(carContext.getString(R.string.settings_action_title))
|
||||
)
|
||||
.setStartHeaderAction(Action.BACK)
|
||||
.build())
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun getTitle(): String {
|
||||
return if (audioToggleState) {
|
||||
carContext.getString(R.string.on_action_title)
|
||||
} else {
|
||||
carContext.getString(R.string.off_action_title)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getImage(): CarIcon {
|
||||
return if (audioToggleState) {
|
||||
CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.volume_up_24px))
|
||||
.build()
|
||||
} else {
|
||||
CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.volume_off_24px))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildRowForTemplate(screen: Screen, title: Int): Row {
|
||||
return Row.Builder()
|
||||
.setTitle(carContext.getString(title))
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.kouros.navigation.car.screen.observers
|
||||
|
||||
import com.kouros.navigation.data.overpass.Elements
|
||||
import com.kouros.navigation.data.overpass.Tags
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.kotlin.*
|
||||
|
||||
class CategoryObserverTest {
|
||||
|
||||
private lateinit var mockCallback: CategoryObserverCallback
|
||||
private lateinit var observer: CategoryObserver
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockCallback = mock()
|
||||
observer = CategoryObserver(mockCallback)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onChanged with empty list does not invoke callback`() {
|
||||
observer.onChanged(emptyList())
|
||||
|
||||
verify(mockCallback, never()).onCategoryElementsReady(any(), any(), any(), any())
|
||||
verify(mockCallback, never()).invalidateScreen()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onChanged with single element invokes both callbacks`() {
|
||||
val element = createElement(lon = 10.5, lat = 52.3)
|
||||
|
||||
observer.onChanged(listOf(element))
|
||||
|
||||
verify(mockCallback).onCategoryElementsReady(any(), any(), any(), any())
|
||||
verify(mockCallback).invalidateScreen()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onChanged uses first element coordinates as map center`() {
|
||||
val first = createElement(lon = 10.5, lat = 52.3)
|
||||
val second = createElement(lon = 11.0, lat = 53.0)
|
||||
|
||||
observer.onChanged(listOf(first, second))
|
||||
|
||||
argumentCaptor<Double>().apply {
|
||||
verify(mockCallback).onCategoryElementsReady(any(), capture(), capture(), any())
|
||||
val capturedLat = firstValue
|
||||
val capturedLon = secondValue
|
||||
assert(capturedLat == 52.3) { "Expected centerLat=52.3 but was $capturedLat" }
|
||||
assert(capturedLon == 10.5) { "Expected centerLon=10.5 but was $capturedLon" }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onChanged passes all elements to callback`() {
|
||||
val elements = listOf(
|
||||
createElement(lon = 10.0, lat = 52.0),
|
||||
createElement(lon = 11.0, lat = 53.0),
|
||||
createElement(lon = 12.0, lat = 54.0)
|
||||
)
|
||||
|
||||
observer.onChanged(elements)
|
||||
|
||||
argumentCaptor<List<Elements>>().apply {
|
||||
verify(mockCallback).onCategoryElementsReady(capture(), any(), any(), any())
|
||||
assert(firstValue.size == 3)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onChanged builds coordinates list with lon first then lat`() {
|
||||
val element = createElement(lon = 10.5, lat = 52.3)
|
||||
|
||||
observer.onChanged(listOf(element))
|
||||
|
||||
argumentCaptor<List<List<Double>>>().apply {
|
||||
verify(mockCallback).onCategoryElementsReady(any(), any(), any(), capture())
|
||||
val coords = firstValue
|
||||
assert(coords.size == 1)
|
||||
assert(coords[0][0] == 10.5) { "Expected lon=10.5 at index 0 but was ${coords[0][0]}" }
|
||||
assert(coords[0][1] == 52.3) { "Expected lat=52.3 at index 1 but was ${coords[0][1]}" }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onChanged collects coordinates for all elements`() {
|
||||
val elements = listOf(
|
||||
createElement(lon = 10.0, lat = 52.0),
|
||||
createElement(lon = 11.0, lat = 53.0)
|
||||
)
|
||||
|
||||
observer.onChanged(elements)
|
||||
|
||||
argumentCaptor<List<List<Double>>>().apply {
|
||||
verify(mockCallback).onCategoryElementsReady(any(), any(), any(), capture())
|
||||
assert(firstValue.size == 2)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createElement(lon: Double, lat: Double): Elements {
|
||||
return Elements(lon = lon, lat = lat, tags = Tags())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user