Auto Drive Simulation
This commit is contained in:
7
README.md
Normal file
7
README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# README.md
|
||||
|
||||
## Introduction
|
||||
|
||||
## Simulation
|
||||
|
||||
adb shell dumpsys activity service com.kouros.navigation.car.NavigationCarAppService AUTO_DRIVE
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
applicationId = "com.kouros.navigation"
|
||||
minSdk = 33
|
||||
targetSdk = 36
|
||||
versionCode = 60
|
||||
versionName = "0.2.0.60"
|
||||
versionCode = 61
|
||||
versionName = "0.2.0.61"
|
||||
base.archivesName = "navi-$versionName"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.kouros.navigation.model
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
|
||||
import com.kouros.navigation.utils.location
|
||||
@@ -9,18 +10,20 @@ import io.ticofab.androidgpxparser.parser.domain.Gpx
|
||||
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.joda.time.DateTime
|
||||
import kotlin.collections.forEach
|
||||
|
||||
var simulationJob: Job? = null
|
||||
fun simulate(routeModel: RouteModel, mock: MockLocation) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
simulationJob?.cancel()
|
||||
simulationJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
var lastLocation = location(0.0, 0.0)
|
||||
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
|
||||
val curLocation = location(waypoint[0], waypoint[1])
|
||||
if (routeModel.isNavigating()) {
|
||||
val deviation = 0.0
|
||||
if (index in 0..routeModel.curRoute.waypoints.size) {
|
||||
val bearing = lastLocation.bearingTo(curLocation)
|
||||
mock.setMockLocation(waypoint[1], waypoint[0], bearing)
|
||||
|
||||
@@ -13,36 +13,30 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.BottomSheetScaffold
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableDoubleStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -52,7 +46,6 @@ import com.google.android.gms.location.FusedLocationProviderClient
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
|
||||
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.homeVogelhart
|
||||
@@ -63,6 +56,7 @@ import com.kouros.navigation.model.RouteModel
|
||||
import com.kouros.navigation.model.SimulationType
|
||||
import com.kouros.navigation.model.gpx
|
||||
import com.kouros.navigation.model.simulate
|
||||
import com.kouros.navigation.model.simulationJob
|
||||
import com.kouros.navigation.model.test
|
||||
import com.kouros.navigation.model.testSingle
|
||||
import com.kouros.navigation.ui.app.AppViewModel
|
||||
@@ -92,6 +86,7 @@ class MainActivity : ComponentActivity() {
|
||||
val routeModel = RouteModel()
|
||||
var tilt = TILT
|
||||
val useMock = false
|
||||
|
||||
val type = SimulationType.SIMULATE
|
||||
val stepData: MutableLiveData<StepData> by lazy {
|
||||
MutableLiveData()
|
||||
@@ -108,11 +103,8 @@ class MainActivity : ComponentActivity() {
|
||||
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
|
||||
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
|
||||
routeModel.startNavigation(newRoute)
|
||||
if (routeModel.hasLegs()) {
|
||||
getSettingsViewModel(applicationContext).onLastRouteChanged(newRoute)
|
||||
}
|
||||
routeData.value = routeModel.curRoute.routeGeoJson
|
||||
checkMock()
|
||||
// checkMock()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +135,13 @@ class MainActivity : ComponentActivity() {
|
||||
private lateinit var mock: MockLocation
|
||||
private var loadRecentPlaces = false
|
||||
|
||||
override fun onDestroy() {
|
||||
if (simulationJob != null) {
|
||||
simulationJob?.cancel()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -371,6 +370,7 @@ class MainActivity : ComponentActivity() {
|
||||
routeModel.stopNavigation()
|
||||
getSettingsViewModel(applicationContext).onLastRouteChanged("")
|
||||
if (useMock) {
|
||||
simulationJob?.cancel()
|
||||
mock.setMockLocation(latitude, longitude, 0F)
|
||||
}
|
||||
routeData.value = ""
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.Manifest.permission
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.util.Log
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
@@ -21,9 +20,10 @@ import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelStore
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kouros.navigation.car.navigation.NavigationUtils
|
||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||
import com.kouros.navigation.car.navigation.Simulation
|
||||
import com.kouros.navigation.car.screen.NavigationScreen
|
||||
import com.kouros.navigation.car.screen.RequestPermissionScreen
|
||||
import com.kouros.navigation.car.screen.SearchScreen
|
||||
@@ -42,8 +42,6 @@ import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
||||
import com.kouros.navigation.utils.getSettingsRepository
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.launch
|
||||
import org.maplibre.compose.expressions.dsl.step
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
/**
|
||||
@@ -76,6 +74,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
|
||||
lateinit var textToSpeechManager: TextToSpeechManager
|
||||
|
||||
var autoDriveEnabled = false
|
||||
|
||||
val simulation = Simulation()
|
||||
/**
|
||||
* Lifecycle observer for managing session lifecycle events.
|
||||
* Cleans up resources when the session is destroyed.
|
||||
@@ -91,9 +92,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
if (::deviceLocationManager.isInitialized) {
|
||||
deviceLocationManager.stopLocationUpdates()
|
||||
}
|
||||
if (::textToSpeechManager.isInitialized) {
|
||||
textToSpeechManager.cleanup()
|
||||
}
|
||||
if (::textToSpeechManager.isInitialized) {
|
||||
textToSpeechManager.cleanup()
|
||||
}
|
||||
Log.i(TAG, "NavigationSession destroyed")
|
||||
}
|
||||
}
|
||||
@@ -200,19 +201,19 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
* Initializes managers for rendering, sensors, and location.
|
||||
*/
|
||||
private fun initializeManagers() {
|
||||
|
||||
navigationManager = carContext.getCarService(NavigationManager::class.java)
|
||||
navigationManager.setNavigationManagerCallback(object : NavigationManagerCallback {
|
||||
override fun onAutoDriveEnabled() {
|
||||
// Called when the app should simulate navigation (e.g., for testing)
|
||||
// Implement your simulation logic here
|
||||
Log.d("CarApp", "Auto Drive Enabled")
|
||||
autoDriveEnabled = true
|
||||
}
|
||||
|
||||
override fun onStopNavigation() {
|
||||
// Called when the user stops navigation in the car screen
|
||||
Log.d("CarApp", "Stop Navigation Requested")
|
||||
// Stop turn-by-turn logic and clean up
|
||||
routeModel.stopNavigation()
|
||||
autoDriveEnabled = false
|
||||
}
|
||||
})
|
||||
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
|
||||
@@ -341,6 +342,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)
|
||||
}
|
||||
updateBearing(location)
|
||||
if (routeModel.isNavigating()) {
|
||||
handleNavigationLocation(location)
|
||||
@@ -364,7 +368,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
*/
|
||||
private fun handleNavigationLocation(location: Location) {
|
||||
if (guidanceAudio == 1) {
|
||||
handleGuidanceAudio()
|
||||
handleGuidanceAudio()
|
||||
}
|
||||
navigationScreen.updateTrip(location)
|
||||
if (routeModel.navState.arrived) return
|
||||
@@ -374,9 +378,11 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
distance > MAXIMAL_ROUTE_DEVIATION -> {
|
||||
navigationScreen.calculateNewRoute(routeModel.navState.destination)
|
||||
}
|
||||
|
||||
distance < MAXIMAL_SNAP_CORRECTION -> {
|
||||
surfaceRenderer.updateLocation(snappedLocation)
|
||||
}
|
||||
|
||||
else -> {
|
||||
surfaceRenderer.updateLocation(location)
|
||||
}
|
||||
@@ -390,6 +396,10 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
override fun stopNavigation() {
|
||||
routeModel.stopNavigation()
|
||||
navigationManager.navigationEnded()
|
||||
if (autoDriveEnabled) {
|
||||
simulation.stopSimulation()
|
||||
autoDriveEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -398,6 +408,13 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
*/
|
||||
override fun startNavigation() {
|
||||
navigationManager.navigationStarted()
|
||||
if (autoDriveEnabled) {
|
||||
simulation.startSimulation(
|
||||
routeModel, lifecycle.coroutineScope
|
||||
) { location ->
|
||||
updateLocation(location)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateTrip(trip: Trip) {
|
||||
@@ -407,7 +424,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
|
||||
/**
|
||||
* Handle guidance audio
|
||||
* Called when user wants to hear the step by step instructions
|
||||
* Called when user wants to hear the step-by-step instructions
|
||||
*/
|
||||
private fun handleGuidanceAudio() {
|
||||
val currentStep = routeModel.route.currentStep()
|
||||
|
||||
@@ -172,7 +172,7 @@ class RouteCarModel : RouteModel() {
|
||||
}
|
||||
|
||||
fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String) {
|
||||
carContext.getCarService<AppManager?>(AppManager::class.java)
|
||||
carContext.getCarService(AppManager::class.java)
|
||||
.showAlert(
|
||||
createAlert(
|
||||
carContext,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.kouros.navigation.car.navigation
|
||||
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.os.SystemClock
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class Simulation {
|
||||
|
||||
private var simulationJob: Job? = null
|
||||
|
||||
fun startSimulation(
|
||||
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 2 seconds)
|
||||
delay(500)
|
||||
lastLocation = fakeLocation
|
||||
}
|
||||
routeModel.stopNavigation()
|
||||
}
|
||||
}
|
||||
|
||||
fun stopSimulation() {
|
||||
simulationJob?.cancel()
|
||||
}
|
||||
}
|
||||
@@ -36,9 +36,12 @@ 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.DESTINATION_ARRIVAL_DISTANCE
|
||||
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
|
||||
@@ -89,13 +92,16 @@ class NavigationScreen(
|
||||
|
||||
val repository = getSettingsRepository(carContext)
|
||||
|
||||
val settingsViewModel = getSettingsViewModel(carContext)
|
||||
|
||||
|
||||
var distanceMode = 0
|
||||
|
||||
init {
|
||||
observerManager.attachAllObservers(this)
|
||||
lifecycleScope.launch {
|
||||
getSettingsViewModel(carContext).routingEngine.first()
|
||||
getSettingsViewModel(carContext).recentPlaces.first()
|
||||
settingsViewModel.routingEngine.first()
|
||||
settingsViewModel.recentPlaces.first()
|
||||
distanceMode = repository.distanceModeFlow.first()
|
||||
}
|
||||
}
|
||||
@@ -111,7 +117,7 @@ class NavigationScreen(
|
||||
navigationType = NavigationType.NAVIGATION
|
||||
routeModel.startNavigation(route)
|
||||
if (routeModel.hasLegs()) {
|
||||
getSettingsViewModel(carContext).onLastRouteChanged(route)
|
||||
settingsViewModel.onLastRouteChanged(route)
|
||||
}
|
||||
surfaceRenderer.setRouteData()
|
||||
listener.startNavigation()
|
||||
@@ -616,30 +622,37 @@ class NavigationScreen(
|
||||
fun updateTrip(location: Location) {
|
||||
val current = LocalDateTime.now(ZoneOffset.UTC)
|
||||
val duration = Duration.between(current, lastTrafficDate)
|
||||
if (duration.abs().seconds > 360) {
|
||||
if (duration.abs().seconds > TRAFFIC_UPDATE) {
|
||||
lastTrafficDate = current
|
||||
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
|
||||
}
|
||||
updateSpeedCamera(location)
|
||||
with(routeModel) {
|
||||
updateLocation( location, navigationViewModel)
|
||||
if ((navState.maneuverType == Maneuver.TYPE_DESTINATION
|
||||
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|
||||
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|
||||
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_STRAIGHT)
|
||||
&& routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
|
||||
) {
|
||||
stopNavigation()
|
||||
getSettingsViewModel(carContext).onLastRouteChanged("")
|
||||
navState = navState.copy(arrived = true)
|
||||
surfaceRenderer.routeData.value = ""
|
||||
navigationType = NavigationType.ARRIVAL
|
||||
invalidate()
|
||||
}
|
||||
checkArrival()
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for arrival
|
||||
*/
|
||||
private fun RouteCarModel.checkArrival() {
|
||||
if ((navState.maneuverType == Maneuver.TYPE_DESTINATION
|
||||
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|
||||
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|
||||
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_STRAIGHT)
|
||||
&& routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
|
||||
) {
|
||||
listener.stopNavigation()
|
||||
settingsViewModel.onLastRouteChanged("")
|
||||
navState = navState.copy(arrived = true)
|
||||
surfaceRenderer.routeData.value = ""
|
||||
navigationType = NavigationType.ARRIVAL
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the trip information and notifies the listener with a new Trip object.
|
||||
* This includes destination name, address, travel estimate, and loading status.
|
||||
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
kotlin("plugin.serialization") version "2.2.21"
|
||||
alias(libs.plugins.kotlin.kapt)
|
||||
//id("com.google.protobuf") version "0.9.6"
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
@@ -125,12 +125,13 @@ object Constants {
|
||||
|
||||
const val MAXIMAL_ROUTE_DEVIATION = 80.0
|
||||
|
||||
const val DESTINATION_ARRIVAL_DISTANCE = 40.0
|
||||
const val DESTINATION_ARRIVAL_DISTANCE = 20.0
|
||||
|
||||
const val NEAREST_LOCATION_DISTANCE = 10F
|
||||
|
||||
const val MAXIMUM_LOCATION_DISTANCE = 100000F
|
||||
|
||||
const val TRAFFIC_UPDATE = 300
|
||||
const val GMS_CAR_SPEED_PERMISSION = "com.google.android.gms.permission.CAR_SPEED"
|
||||
|
||||
const val AUTOMOTIVE_CAR_SPEED_PERMISSION = "android.car.permission.CAR_SPEED"
|
||||
|
||||
@@ -21,6 +21,9 @@ private const val tomtomFields =
|
||||
|
||||
const val useAsset = false
|
||||
|
||||
const val useAssetTraffic = false
|
||||
|
||||
|
||||
class TomTomRepository : NavigationRepository() {
|
||||
override fun getRoute(
|
||||
context: Context,
|
||||
@@ -63,7 +66,7 @@ class TomTomRepository : NavigationRepository() {
|
||||
val repository = getSettingsRepository(context)
|
||||
val tomtomApiKey = runBlocking { repository.tomTomApiKeyFlow.first() }
|
||||
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
|
||||
return if (useAsset) {
|
||||
return if (useAssetTraffic) {
|
||||
val trafficJson = context.resources.openRawResource(R.raw.tomtom_traffic)
|
||||
trafficJson.bufferedReader().use { it.readText() }
|
||||
} else {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[versions]
|
||||
agp = "9.0.1"
|
||||
agp = "9.1.0"
|
||||
androidGpxParser = "2.3.1"
|
||||
androidSdkTurf = "6.0.1"
|
||||
datastore = "1.2.0"
|
||||
gradle = "9.0.1"
|
||||
gradle = "9.1.0"
|
||||
koinAndroid = "4.1.1"
|
||||
koinAndroidxCompose = "4.1.1"
|
||||
koinComposeViewmodel = "4.1.1"
|
||||
@@ -21,7 +21,7 @@ material = "1.13.0"
|
||||
carApp = "1.7.0"
|
||||
androidx-car = "1.7.0"
|
||||
materialIconsExtended = "1.7.8"
|
||||
mockitoCore = "5.21.0"
|
||||
mockitoCore = "5.22.0"
|
||||
mockitoKotlin = "6.2.3"
|
||||
rules = "1.7.0"
|
||||
runner = "1.7.0"
|
||||
|
||||
Reference in New Issue
Block a user