mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +01:00
stream directions to car directly
This commit is contained in:
1 parent
c57abecc62
commit
dd3b845c7f
5 files changed
+235
-4
No files matched your search
@@ -4,6 +4,7 @@ import android.os.Build
|
|||||||
import app.seb3thehacker.gearslip.audio.AudioLink
|
import app.seb3thehacker.gearslip.audio.AudioLink
|
||||||
import app.seb3thehacker.gearslip.audio.CarMic
|
import app.seb3thehacker.gearslip.audio.CarMic
|
||||||
import app.seb3thehacker.gearslip.media.CarMediaStatus
|
import app.seb3thehacker.gearslip.media.CarMediaStatus
|
||||||
|
import app.seb3thehacker.gearslip.car.ClusterNav
|
||||||
import app.seb3thehacker.gearslip.car.CarAssistant
|
import app.seb3thehacker.gearslip.car.CarAssistant
|
||||||
import app.seb3thehacker.gearslip.car.CarEnvironment
|
import app.seb3thehacker.gearslip.car.CarEnvironment
|
||||||
import app.seb3thehacker.gearslip.car.CarKeys
|
import app.seb3thehacker.gearslip.car.CarKeys
|
||||||
@@ -70,6 +71,7 @@ class GearslipRunner(
|
|||||||
@Volatile private var inputKeycodes: List<Int> = emptyList()
|
@Volatile private var inputKeycodes: List<Int> = emptyList()
|
||||||
@Volatile private var sensorChannelId: Int = -1
|
@Volatile private var sensorChannelId: Int = -1
|
||||||
@Volatile private var mediaStatus: CarMediaStatus? = null
|
@Volatile private var mediaStatus: CarMediaStatus? = null
|
||||||
|
@Volatile private var clusterNav: ClusterNav? = null
|
||||||
private var sensorTypes: List<Int> = emptyList()
|
private var sensorTypes: List<Int> = emptyList()
|
||||||
@Volatile private var audioLink: AudioLink? = null
|
@Volatile private var audioLink: AudioLink? = null
|
||||||
private var audioMessagesSeen = 0
|
private var audioMessagesSeen = 0
|
||||||
@@ -145,6 +147,8 @@ class GearslipRunner(
|
|||||||
CarMic.current = null
|
CarMic.current = null
|
||||||
mediaStatus?.stop()
|
mediaStatus?.stop()
|
||||||
mediaStatus = null
|
mediaStatus = null
|
||||||
|
clusterNav?.stop()
|
||||||
|
clusterNav = null
|
||||||
lastSensorNight = null
|
lastSensorNight = null
|
||||||
CarEnvironment.setSensorNight(null) // back to the clock until a car says otherwise
|
CarEnvironment.setSensorNight(null) // back to the clock until a car says otherwise
|
||||||
projection?.onProjectionStopped()
|
projection?.onProjectionStopped()
|
||||||
@@ -180,6 +184,8 @@ class GearslipRunner(
|
|||||||
onInputMessage(messageId, body)
|
onInputMessage(messageId, body)
|
||||||
} else if (frame.channel == sensorChannelId) {
|
} else if (frame.channel == sensorChannelId) {
|
||||||
onSensorMessage(messageId, body)
|
onSensorMessage(messageId, body)
|
||||||
|
} else if (frame.channel == clusterNav?.channelId) {
|
||||||
|
clusterNav?.onMessage(messageId, body)
|
||||||
} else if (frame.channel == mediaStatus?.channelId) {
|
} else if (frame.channel == mediaStatus?.channelId) {
|
||||||
mediaStatus?.onMessage(messageId, body)
|
mediaStatus?.onMessage(messageId, body)
|
||||||
} else if (frame.channel == CarMic.current?.channelId) {
|
} else if (frame.channel == CarMic.current?.channelId) {
|
||||||
@@ -424,6 +430,7 @@ class GearslipRunner(
|
|||||||
startSensorChannel(serviceDiscoveryResponse)
|
startSensorChannel(serviceDiscoveryResponse)
|
||||||
startMicChannel(serviceDiscoveryResponse)
|
startMicChannel(serviceDiscoveryResponse)
|
||||||
startMediaStatusChannel(serviceDiscoveryResponse)
|
startMediaStatusChannel(serviceDiscoveryResponse)
|
||||||
|
startClusterNavChannel(serviceDiscoveryResponse)
|
||||||
|
|
||||||
val input = ServiceDiscovery.findInputService(serviceDiscoveryResponse, video.displayId)
|
val input = ServiceDiscovery.findInputService(serviceDiscoveryResponse, video.displayId)
|
||||||
if (input == null) {
|
if (input == null) {
|
||||||
@@ -496,6 +503,17 @@ class GearslipRunner(
|
|||||||
openChannel(channel)
|
openChannel(channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Turn-by-turn for the instrument cluster: the next turn, its road, and how far. */
|
||||||
|
private fun startClusterNavChannel(serviceDiscoveryResponse: ByteArray) {
|
||||||
|
val channel = ServiceDiscovery.findNavStatusChannel(serviceDiscoveryResponse) ?: run {
|
||||||
|
log.i("no navigation status channel - the cluster won't show turns")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.i("navigation status channel: $channel")
|
||||||
|
clusterNav = ClusterNav(channel) { id, body -> send(id, body, encrypted = true, channel = channel) }
|
||||||
|
openChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every sensor type the head unit advertised, subscribed to at once - there is no screen
|
* Every sensor type the head unit advertised, subscribed to at once - there is no screen
|
||||||
* to pick and choose yet, this is meant to show everything reachable. [CarSensors] answers
|
* to pick and choose yet, this is meant to show everything reachable. [CarSensors] answers
|
||||||
|
|||||||
@@ -114,10 +114,15 @@ object ServiceDiscovery {
|
|||||||
* The channel the car takes "now playing" on (MediaPlaybackStatusService, Service field 9),
|
* The channel the car takes "now playing" on (MediaPlaybackStatusService, Service field 9),
|
||||||
* which feeds its cluster and media screens. It carries no settings. Null when the car has none.
|
* which feeds its cluster and media screens. It carries no settings. Null when the car has none.
|
||||||
*/
|
*/
|
||||||
fun findMediaStatusChannel(response: ByteArray): Int? =
|
fun findMediaStatusChannel(response: ByteArray): Int? = findChannelWith(response, 9)
|
||||||
|
|
||||||
|
/** The instrument cluster's turn-by-turn channel (NavigationStatusService, Service field 8). */
|
||||||
|
fun findNavStatusChannel(response: ByteArray): Int? = findChannelWith(response, 8)
|
||||||
|
|
||||||
|
private fun findChannelWith(response: ByteArray, serviceField: Int): Int? =
|
||||||
Wire.allBytes(Wire.fields(response), 1).firstNotNullOfOrNull { channel ->
|
Wire.allBytes(Wire.fields(response), 1).firstNotNullOfOrNull { channel ->
|
||||||
val service = Wire.fields(channel)
|
val service = Wire.fields(channel)
|
||||||
if (service.any { it.number == 9 }) Wire.varint(service, 1)?.toInt() else null
|
if (service.any { it.number == serviceField }) Wire.varint(service, 1)?.toInt() else null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ object CarServices {
|
|||||||
val nav: CarAppConnection by lazy { CarAppConnection(requireNotNull(context)) }
|
val nav: CarAppConnection by lazy { CarAppConnection(requireNotNull(context)) }
|
||||||
val media: CarMedia by lazy { CarMedia(requireNotNull(context)) }
|
val media: CarMedia by lazy { CarMedia(requireNotNull(context)) }
|
||||||
|
|
||||||
|
/** [nav], or null before the car screen has set things up. */
|
||||||
|
fun navOrNull(): CarAppConnection? = if (context != null) nav else null
|
||||||
|
|
||||||
/** [media], or null before the car screen has set things up: for code outside the car UI. */
|
/** [media], or null before the car screen has set things up: for code outside the car UI. */
|
||||||
fun mediaOrNull(): CarMedia? = if (context != null) media else null
|
fun mediaOrNull(): CarMedia? = if (context != null) media else null
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.car.app.model.CarText
|
||||||
|
import androidx.car.app.model.Distance
|
||||||
|
import androidx.car.app.navigation.model.Maneuver
|
||||||
|
import androidx.car.app.navigation.model.NavigationTemplate
|
||||||
|
import androidx.car.app.navigation.model.RoutingInfo
|
||||||
|
import androidx.car.app.navigation.model.Step
|
||||||
|
import app.seb3thehacker.gearslip.GearslipLog
|
||||||
|
import app.seb3thehacker.gearslip.Protobuf
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The next turn on the car's instrument cluster, over its NavigationStatusService (Service
|
||||||
|
* field 8), the way Android Auto feeds it:
|
||||||
|
*
|
||||||
|
* -> NavigationStatus { status = 1 (1 active, 2 inactive, 3 rerouting) }
|
||||||
|
* -> NavigationNextTurnEvent { road = 1; turn_side = 2; event = 3; turn_number = 5; turn_angle = 6 }
|
||||||
|
* -> NavigationNextTurnDistanceEvent { distance_meters = 1; time_to_turn_seconds = 2;
|
||||||
|
* display_distance_e3 = 3; display_distance_unit = 4 }
|
||||||
|
*
|
||||||
|
* Newer cars also take a NavigationState message; the deprecated pair above is what a 2018
|
||||||
|
* Uconnect understands. The turn comes from the map app's trip (what the Car App Library hands
|
||||||
|
* to clusters), or from its routing card when it sends no trip.
|
||||||
|
*/
|
||||||
|
class ClusterNav(
|
||||||
|
val channelId: Int,
|
||||||
|
private val sendOnChannel: (messageId: Int, body: ByteArray) -> Unit,
|
||||||
|
) {
|
||||||
|
@Volatile private var running = false
|
||||||
|
private var sentStatus = -1
|
||||||
|
private var sentTurn: String? = null
|
||||||
|
private var sentDistance: String? = null
|
||||||
|
|
||||||
|
fun onMessage(messageId: Int, body: ByteArray) {
|
||||||
|
when (messageId) {
|
||||||
|
MSG_CHANNEL_OPEN_RESPONSE -> {
|
||||||
|
val status = Protobuf.readInt32Field(body, 1) ?: 0
|
||||||
|
GearslipLog.i("cluster: <- ChannelOpenResponse: status=$status")
|
||||||
|
if (status == 0) start()
|
||||||
|
}
|
||||||
|
// The car saying when it wants turns. Sending all along is harmless, so these are only logged.
|
||||||
|
MSG_START -> GearslipLog.i("cluster: <- start")
|
||||||
|
MSG_STOP -> GearslipLog.i("cluster: <- stop")
|
||||||
|
else -> GearslipLog.i("cluster: <- unhandled message id=$messageId (${body.size} bytes)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun start() {
|
||||||
|
if (running) return
|
||||||
|
running = true
|
||||||
|
Thread({
|
||||||
|
while (running) {
|
||||||
|
runCatching { tick() }.onFailure { GearslipLog.w("cluster: ${it.message}") }
|
||||||
|
Thread.sleep(TICK_MS)
|
||||||
|
}
|
||||||
|
}, "gearslip-cluster").start()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
running = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One turn as the car wants it, from whichever source the map app fills. */
|
||||||
|
private class Turn(val step: Step, val distance: Distance?, val seconds: Long?, val rerouting: Boolean)
|
||||||
|
|
||||||
|
private fun current(): Turn? {
|
||||||
|
val nav = CarServices.navOrNull() ?: return null
|
||||||
|
nav.trip.value?.let { trip ->
|
||||||
|
val step = trip.steps.firstOrNull() ?: return@let
|
||||||
|
val estimate = trip.stepTravelEstimates.firstOrNull()
|
||||||
|
return Turn(step, estimate?.remainingDistance, estimate?.remainingTimeSeconds?.takeIf { it >= 0 }, trip.isLoading)
|
||||||
|
}
|
||||||
|
val info = (nav.template.value as? NavigationTemplate)?.navigationInfo as? RoutingInfo ?: return null
|
||||||
|
val step = info.currentStep ?: return null
|
||||||
|
return Turn(step, info.currentDistance, null, info.isLoading)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tick() {
|
||||||
|
val turn = current()
|
||||||
|
val status = when {
|
||||||
|
turn == null -> STATUS_INACTIVE
|
||||||
|
turn.rerouting -> STATUS_REROUTING
|
||||||
|
else -> STATUS_ACTIVE
|
||||||
|
}
|
||||||
|
if (status != sentStatus) {
|
||||||
|
sendOnChannel(MSG_STATUS, Protobuf.varintField(1, status.toLong()))
|
||||||
|
GearslipLog.i("cluster: -> ${STATUS_NAMES[status]}")
|
||||||
|
sentStatus = status
|
||||||
|
if (status == STATUS_INACTIVE) { sentTurn = null; sentDistance = null }
|
||||||
|
}
|
||||||
|
if (turn == null || status == STATUS_REROUTING) return
|
||||||
|
sendTurn(turn.step)
|
||||||
|
turn.distance?.let { sendDistance(it, turn.seconds) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendTurn(step: Step) {
|
||||||
|
val maneuver = step.maneuver
|
||||||
|
val (event, side) = maneuver?.let { eventFor(it.type) } ?: (EVENT_UNKNOWN to SIDE_UNSPECIFIED)
|
||||||
|
val road = step.road.text().ifBlank { step.cue.text() }
|
||||||
|
val key = "$road|$event|$side|${maneuver?.roundaboutExitNumber}"
|
||||||
|
if (key == sentTurn) return
|
||||||
|
sentTurn = key
|
||||||
|
var body = Protobuf.stringField(1, road) +
|
||||||
|
Protobuf.varintField(2, side.toLong()) +
|
||||||
|
Protobuf.varintField(3, event.toLong())
|
||||||
|
maneuver?.roundaboutExitNumber?.takeIf { it > 0 }?.let { body += Protobuf.varintField(5, it.toLong()) }
|
||||||
|
maneuver?.roundaboutExitAngle?.takeIf { it > 0 }?.let { body += Protobuf.varintField(6, it.toLong()) }
|
||||||
|
sendOnChannel(MSG_TURN, body)
|
||||||
|
GearslipLog.i("cluster: -> turn event=$event side=$side road=\"$road\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Car App Library's distance units use the same numbers as the protocol's (1 metres ...
|
||||||
|
* 7 yards), so the app's own display choice passes straight through.
|
||||||
|
*/
|
||||||
|
private fun sendDistance(distance: Distance, seconds: Long?) {
|
||||||
|
val meters = toMeters(distance)
|
||||||
|
val key = "${distance.displayDistance}|${distance.displayUnit}|${seconds ?: -1}"
|
||||||
|
if (key == sentDistance) return
|
||||||
|
sentDistance = key
|
||||||
|
sendOnChannel(
|
||||||
|
MSG_DISTANCE,
|
||||||
|
Protobuf.varintField(1, meters.toLong()) +
|
||||||
|
Protobuf.varintField(2, (seconds ?: 0L).coerceAtLeast(0)) +
|
||||||
|
Protobuf.varintField(3, (distance.displayDistance * 1000).toLong()) +
|
||||||
|
Protobuf.varintField(4, distance.displayUnit.toLong()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toMeters(d: Distance): Int = (d.displayDistance * when (d.displayUnit) {
|
||||||
|
Distance.UNIT_KILOMETERS, Distance.UNIT_KILOMETERS_P1 -> 1000.0
|
||||||
|
Distance.UNIT_MILES, Distance.UNIT_MILES_P1 -> 1609.344
|
||||||
|
Distance.UNIT_FEET -> 0.3048
|
||||||
|
Distance.UNIT_YARDS -> 0.9144
|
||||||
|
else -> 1.0
|
||||||
|
}).toInt()
|
||||||
|
|
||||||
|
private fun CarText?.text() = this?.toString().orEmpty()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TICK_MS = 500L
|
||||||
|
private const val MSG_CHANNEL_OPEN_RESPONSE = 8
|
||||||
|
private const val MSG_START = 32769
|
||||||
|
private const val MSG_STOP = 32770
|
||||||
|
private const val MSG_STATUS = 32771
|
||||||
|
private const val MSG_TURN = 32772
|
||||||
|
private const val MSG_DISTANCE = 32773
|
||||||
|
|
||||||
|
private const val STATUS_ACTIVE = 1
|
||||||
|
private const val STATUS_INACTIVE = 2
|
||||||
|
private const val STATUS_REROUTING = 3
|
||||||
|
private val STATUS_NAMES = mapOf(1 to "navigating", 2 to "not navigating", 3 to "rerouting")
|
||||||
|
|
||||||
|
private const val SIDE_LEFT = 1
|
||||||
|
private const val SIDE_RIGHT = 2
|
||||||
|
private const val SIDE_UNSPECIFIED = 3
|
||||||
|
|
||||||
|
private const val EVENT_UNKNOWN = 0
|
||||||
|
|
||||||
|
/** The car's turn icon and side for a Car App Library manoeuvre. */
|
||||||
|
fun eventFor(type: Int): Pair<Int, Int> = when (type) {
|
||||||
|
Maneuver.TYPE_DEPART -> 1 to SIDE_UNSPECIFIED
|
||||||
|
Maneuver.TYPE_NAME_CHANGE -> 2 to SIDE_UNSPECIFIED
|
||||||
|
Maneuver.TYPE_KEEP_LEFT, Maneuver.TYPE_TURN_SLIGHT_LEFT -> 3 to SIDE_LEFT
|
||||||
|
Maneuver.TYPE_KEEP_RIGHT, Maneuver.TYPE_TURN_SLIGHT_RIGHT -> 3 to SIDE_RIGHT
|
||||||
|
Maneuver.TYPE_TURN_NORMAL_LEFT -> 4 to SIDE_LEFT
|
||||||
|
Maneuver.TYPE_TURN_NORMAL_RIGHT -> 4 to SIDE_RIGHT
|
||||||
|
Maneuver.TYPE_TURN_SHARP_LEFT -> 5 to SIDE_LEFT
|
||||||
|
Maneuver.TYPE_TURN_SHARP_RIGHT -> 5 to SIDE_RIGHT
|
||||||
|
Maneuver.TYPE_U_TURN_LEFT -> 6 to SIDE_LEFT
|
||||||
|
Maneuver.TYPE_U_TURN_RIGHT -> 6 to SIDE_RIGHT
|
||||||
|
Maneuver.TYPE_ON_RAMP_SLIGHT_LEFT, Maneuver.TYPE_ON_RAMP_NORMAL_LEFT, Maneuver.TYPE_ON_RAMP_SHARP_LEFT,
|
||||||
|
Maneuver.TYPE_ON_RAMP_U_TURN_LEFT -> 7 to SIDE_LEFT
|
||||||
|
Maneuver.TYPE_ON_RAMP_SLIGHT_RIGHT, Maneuver.TYPE_ON_RAMP_NORMAL_RIGHT, Maneuver.TYPE_ON_RAMP_SHARP_RIGHT,
|
||||||
|
Maneuver.TYPE_ON_RAMP_U_TURN_RIGHT -> 7 to SIDE_RIGHT
|
||||||
|
Maneuver.TYPE_OFF_RAMP_SLIGHT_LEFT, Maneuver.TYPE_OFF_RAMP_NORMAL_LEFT -> 8 to SIDE_LEFT
|
||||||
|
Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT, Maneuver.TYPE_OFF_RAMP_NORMAL_RIGHT -> 8 to SIDE_RIGHT
|
||||||
|
Maneuver.TYPE_FORK_LEFT -> 9 to SIDE_LEFT
|
||||||
|
Maneuver.TYPE_FORK_RIGHT -> 9 to SIDE_RIGHT
|
||||||
|
Maneuver.TYPE_MERGE_LEFT -> 10 to SIDE_LEFT
|
||||||
|
Maneuver.TYPE_MERGE_RIGHT -> 10 to SIDE_RIGHT
|
||||||
|
Maneuver.TYPE_MERGE_SIDE_UNSPECIFIED -> 10 to SIDE_UNSPECIFIED
|
||||||
|
Maneuver.TYPE_ROUNDABOUT_ENTER_CW, Maneuver.TYPE_ROUNDABOUT_ENTER_CCW -> 11 to SIDE_UNSPECIFIED
|
||||||
|
Maneuver.TYPE_ROUNDABOUT_EXIT_CW, Maneuver.TYPE_ROUNDABOUT_EXIT_CCW -> 12 to SIDE_UNSPECIFIED
|
||||||
|
Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW, Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE,
|
||||||
|
Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW, Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE -> 13 to SIDE_UNSPECIFIED
|
||||||
|
Maneuver.TYPE_STRAIGHT -> 14 to SIDE_UNSPECIFIED
|
||||||
|
Maneuver.TYPE_FERRY_BOAT, Maneuver.TYPE_FERRY_BOAT_LEFT, Maneuver.TYPE_FERRY_BOAT_RIGHT -> 16 to SIDE_UNSPECIFIED
|
||||||
|
Maneuver.TYPE_FERRY_TRAIN, Maneuver.TYPE_FERRY_TRAIN_LEFT, Maneuver.TYPE_FERRY_TRAIN_RIGHT -> 17 to SIDE_UNSPECIFIED
|
||||||
|
Maneuver.TYPE_DESTINATION, Maneuver.TYPE_DESTINATION_STRAIGHT -> 19 to SIDE_UNSPECIFIED
|
||||||
|
Maneuver.TYPE_DESTINATION_LEFT -> 19 to SIDE_LEFT
|
||||||
|
Maneuver.TYPE_DESTINATION_RIGHT -> 19 to SIDE_RIGHT
|
||||||
|
else -> EVENT_UNKNOWN to SIDE_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -587,10 +587,19 @@ class CarAppConnection(private val context: Context) {
|
|||||||
override fun openMicrophone(request: Bundleable?): Bundleable? = null
|
override fun openMicrophone(request: Bundleable?): Bundleable? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _trip = MutableStateFlow<androidx.car.app.navigation.model.Trip?>(null)
|
||||||
|
/** The route as the app tells clusters about it: steps and how far to each. Null when not navigating. */
|
||||||
|
val trip: StateFlow<androidx.car.app.navigation.model.Trip?> = _trip.asStateFlow()
|
||||||
|
|
||||||
private val navigationHost = object : INavigationHost.Stub() {
|
private val navigationHost = object : INavigationHost.Stub() {
|
||||||
override fun navigationStarted() = GearslipLog.i("host: navigation started")
|
override fun navigationStarted() = GearslipLog.i("host: navigation started")
|
||||||
override fun navigationEnded() = GearslipLog.i("host: navigation ended")
|
override fun navigationEnded() {
|
||||||
override fun updateTrip(trip: Bundleable?) = Unit
|
GearslipLog.i("host: navigation ended")
|
||||||
|
_trip.value = null
|
||||||
|
}
|
||||||
|
override fun updateTrip(trip: Bundleable?) {
|
||||||
|
_trip.value = runCatching { trip?.get() as? androidx.car.app.navigation.model.Trip }.getOrNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in new issue
Block a user