Inicio de aplicacion
This commit is contained in:
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="25" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -5,6 +5,12 @@
|
|||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="jbr-25" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PlanningModeManager">
|
||||||
|
<option name="approvalStates">
|
||||||
|
<map>
|
||||||
|
<entry key="f9d713bb-3de6-48b7-9013-a05b442ed5a5" value="false" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -39,11 +39,16 @@ dependencies {
|
|||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
|
implementation(libs.androidx.compose.material.icons)
|
||||||
implementation(libs.androidx.compose.ui)
|
implementation(libs.androidx.compose.ui)
|
||||||
implementation(libs.androidx.compose.ui.graphics)
|
implementation(libs.androidx.compose.ui.graphics)
|
||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
implementation(libs.androidx.car.app)
|
||||||
|
implementation(libs.retrofit)
|
||||||
|
implementation(libs.retrofit.gson)
|
||||||
|
implementation(libs.play.services.location)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||||
|
|||||||
@@ -2,6 +2,17 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.type.automotive"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.software.car.templates_host"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
@@ -11,6 +22,14 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Prueba">
|
android:theme="@style/Theme.Prueba">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.gms.car.application"
|
||||||
|
android:resource="@xml/automotive_app_desc" />
|
||||||
|
<meta-data
|
||||||
|
android:name="androidx.car.app.minCarApiLevel"
|
||||||
|
android:value="1" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -23,6 +42,18 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".MyCarAppService"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:permission="androidx.car.app.PERMISSION_CAR_APP">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="androidx.car.app.CarAppService" />
|
||||||
|
<category android:name="androidx.car.app.category.POI" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,29 +1,382 @@
|
|||||||
package com.example.prueba
|
package com.example.prueba
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.location.Geocoder
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.example.prueba.data.GasStationInfo
|
||||||
|
import com.example.prueba.data.GasStationRepository
|
||||||
import com.example.prueba.ui.theme.PruebaTheme
|
import com.example.prueba.ui.theme.PruebaTheme
|
||||||
|
import com.example.prueba.utils.FavoriteStorage
|
||||||
|
import com.example.prueba.utils.FuelRecord
|
||||||
|
import com.example.prueba.utils.FuelStorage
|
||||||
|
import com.example.prueba.utils.LocationStorage
|
||||||
|
import com.google.android.gms.location.*
|
||||||
|
import com.google.android.gms.tasks.CancellationTokenSource
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
private val repository = GasStationRepository()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
PruebaTheme {
|
PruebaTheme {
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
GasStationApp(repository)
|
||||||
Greeting(
|
}
|
||||||
name = "Android",
|
}
|
||||||
modifier = Modifier.padding(innerPadding)
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun GasStationApp(repository: GasStationRepository) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var stations by remember { mutableStateOf<List<GasStationInfo>>(emptyList()) }
|
||||||
|
var lastLocation by remember { mutableStateOf<android.location.Location?>(null) }
|
||||||
|
var fuelRecords by remember { mutableStateOf(FuelStorage.getRecords(context)) }
|
||||||
|
var showFuelDialog by remember { mutableStateOf<GasStationInfo?>(null) }
|
||||||
|
var editRecordDialog by remember { mutableStateOf<FuelRecord?>(null) }
|
||||||
|
var currentAddress by remember { mutableStateOf("Buscando ubicación...") }
|
||||||
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
|
var selectedTab by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
var hasPermission by remember {
|
||||||
|
mutableStateOf(
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val launcher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.RequestPermission()
|
||||||
|
) { isGranted ->
|
||||||
|
hasPermission = isGranted
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateAddress(location: android.location.Location) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val geocoder = Geocoder(context, Locale.getDefault())
|
||||||
|
val addresses = geocoder.getFromLocation(location.latitude, location.longitude, 1)
|
||||||
|
if (!addresses.isNullOrEmpty()) {
|
||||||
|
val address = addresses[0]
|
||||||
|
val street = address.thoroughfare
|
||||||
|
val number = address.subThoroughfare
|
||||||
|
|
||||||
|
currentAddress = if (street != null && number != null) {
|
||||||
|
"$street, $number"
|
||||||
|
} else {
|
||||||
|
street ?: address.getAddressLine(0) ?: "Ubicación desconocida"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ignore: Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(hasPermission) {
|
||||||
|
if (hasPermission) {
|
||||||
|
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
|
||||||
|
LocationStorage.getLastLocation(context)?.let { lastLoc ->
|
||||||
|
scope.launch {
|
||||||
|
updateAddress(lastLoc)
|
||||||
|
stations = repository.getCheapestAndNearest(context, lastLoc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 10000)
|
||||||
|
.setMinUpdateDistanceMeters(500f)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val locationCallback = object : LocationCallback() {
|
||||||
|
override fun onLocationResult(result: LocationResult) {
|
||||||
|
val location = result.lastLocation ?: return
|
||||||
|
lastLocation = location
|
||||||
|
LocationStorage.saveLocation(context, location)
|
||||||
|
scope.launch {
|
||||||
|
updateAddress(location)
|
||||||
|
stations = repository.getCheapestAndNearest(context, location)
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, android.os.Looper.getMainLooper())
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text("Gasolineras Baratas", style = MaterialTheme.typography.titleSmall)
|
||||||
|
Text(
|
||||||
|
text = currentAddress,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
NavigationBar {
|
||||||
|
NavigationBarItem(
|
||||||
|
icon = { Icon(Icons.Default.Place, "Gasolineras") },
|
||||||
|
label = { Text("Gasolineras") },
|
||||||
|
selected = selectedTab == 0,
|
||||||
|
onClick = { selectedTab = 0 }
|
||||||
|
)
|
||||||
|
NavigationBarItem(
|
||||||
|
icon = { Icon(Icons.Default.Favorite, "Favoritas") },
|
||||||
|
label = { Text("Favoritas") },
|
||||||
|
selected = selectedTab == 1,
|
||||||
|
onClick = { selectedTab = 1 }
|
||||||
|
)
|
||||||
|
NavigationBarItem(
|
||||||
|
icon = { Icon(Icons.Default.DateRange, "Historial") },
|
||||||
|
label = { Text("Historial") },
|
||||||
|
selected = selectedTab == 2,
|
||||||
|
onClick = { selectedTab = 2 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Box(modifier = Modifier.padding(innerPadding).fillMaxSize()) {
|
||||||
|
if (!hasPermission) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text("Se necesita permiso de ubicación")
|
||||||
|
Button(onClick = { launcher.launch(Manifest.permission.ACCESS_FINE_LOCATION) }) {
|
||||||
|
Text("Dar permiso")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when (selectedTab) {
|
||||||
|
0, 1 -> {
|
||||||
|
PullToRefreshBox(
|
||||||
|
isRefreshing = isRefreshing,
|
||||||
|
onRefresh = {
|
||||||
|
lastLocation?.let { loc ->
|
||||||
|
isRefreshing = true
|
||||||
|
scope.launch {
|
||||||
|
stations = repository.getCheapestAndNearest(context, loc, forceRefresh = true)
|
||||||
|
isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
if (selectedTab == 0) {
|
||||||
|
if (isLoading && stations.isEmpty()) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||||
|
} else if (stations.isEmpty()) {
|
||||||
|
Text("No se encontraron gasolineras", modifier = Modifier.align(Alignment.Center))
|
||||||
|
} else {
|
||||||
|
GasStationList(
|
||||||
|
stations = stations,
|
||||||
|
context = context,
|
||||||
|
scope = scope,
|
||||||
|
repository = repository,
|
||||||
|
lastLocation = lastLocation,
|
||||||
|
onLogClick = { showFuelDialog = it },
|
||||||
|
onUpdateStations = { stations = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val favoriteStations = stations.filter { it.isFavorite }
|
||||||
|
if (favoriteStations.isEmpty()) {
|
||||||
|
Text("No tienes gasolineras favoritas marcadas", modifier = Modifier.align(Alignment.Center))
|
||||||
|
} else {
|
||||||
|
GasStationList(
|
||||||
|
stations = favoriteStations,
|
||||||
|
context = context,
|
||||||
|
scope = scope,
|
||||||
|
repository = repository,
|
||||||
|
lastLocation = lastLocation,
|
||||||
|
onLogClick = { showFuelDialog = it },
|
||||||
|
onUpdateStations = { stations = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
FuelHistoryScreen(
|
||||||
|
records = fuelRecords,
|
||||||
|
onDelete = { record ->
|
||||||
|
FuelStorage.deleteRecord(context, record)
|
||||||
|
fuelRecords = FuelStorage.getRecords(context)
|
||||||
|
},
|
||||||
|
onEdit = { record ->
|
||||||
|
editRecordDialog = record
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showFuelDialog?.let { station ->
|
||||||
|
FuelLogDialog(
|
||||||
|
stationName = station.name,
|
||||||
|
pricePerLiter = station.price,
|
||||||
|
onDismiss = { showFuelDialog = null },
|
||||||
|
onConfirm = { liters, total ->
|
||||||
|
val record = FuelRecord(
|
||||||
|
date = System.currentTimeMillis(),
|
||||||
|
stationName = station.name,
|
||||||
|
address = station.address,
|
||||||
|
municipality = station.municipality,
|
||||||
|
liters = liters,
|
||||||
|
totalPrice = total,
|
||||||
|
pricePerLiter = station.price
|
||||||
|
)
|
||||||
|
FuelStorage.saveRecord(context, record)
|
||||||
|
fuelRecords = FuelStorage.getRecords(context)
|
||||||
|
showFuelDialog = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
editRecordDialog?.let { record ->
|
||||||
|
FuelLogDialog(
|
||||||
|
stationName = record.stationName,
|
||||||
|
pricePerLiter = record.pricePerLiter,
|
||||||
|
initialLiters = record.liters.toString(),
|
||||||
|
initialTotal = record.totalPrice.toString(),
|
||||||
|
onDismiss = { editRecordDialog = null },
|
||||||
|
onConfirm = { liters, total ->
|
||||||
|
val newRecord = record.copy(
|
||||||
|
liters = liters,
|
||||||
|
totalPrice = total
|
||||||
|
)
|
||||||
|
FuelStorage.updateRecord(context, record, newRecord)
|
||||||
|
fuelRecords = FuelStorage.getRecords(context)
|
||||||
|
editRecordDialog = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GasStationList(
|
||||||
|
stations: List<GasStationInfo>,
|
||||||
|
context: android.content.Context,
|
||||||
|
scope: kotlinx.coroutines.CoroutineScope,
|
||||||
|
repository: GasStationRepository,
|
||||||
|
lastLocation: android.location.Location?,
|
||||||
|
onLogClick: (GasStationInfo) -> Unit,
|
||||||
|
onUpdateStations: (List<GasStationInfo>) -> Unit
|
||||||
|
) {
|
||||||
|
LazyColumn(contentPadding = PaddingValues(vertical = 8.dp)) {
|
||||||
|
items(stations) { station ->
|
||||||
|
GasStationItem(
|
||||||
|
station = station,
|
||||||
|
onClick = {
|
||||||
|
val uri = Uri.parse("geo:${station.latitude},${station.longitude}?q=${station.latitude},${station.longitude}(${Uri.encode(station.name)})")
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||||
|
context.startActivity(intent)
|
||||||
|
},
|
||||||
|
onLogClick = { onLogClick(station) },
|
||||||
|
onFavoriteClick = {
|
||||||
|
FavoriteStorage.toggleFavorite(context, station.id)
|
||||||
|
lastLocation?.let { loc ->
|
||||||
|
scope.launch {
|
||||||
|
onUpdateStations(repository.getCheapestAndNearest(context, loc))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FuelHistoryScreen(records: List<FuelRecord>, onDelete: (FuelRecord) -> Unit, onEdit: (FuelRecord) -> Unit) {
|
||||||
|
if (records.isEmpty()) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Text("Aún no tienes repostajes registrados")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(contentPadding = PaddingValues(16.dp)) {
|
||||||
|
items(records) { record ->
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(record.stationName, style = MaterialTheme.typography.titleMedium)
|
||||||
|
Text("${record.municipality} - ${record.address}", style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
IconButton(onClick = { onEdit(record) }, modifier = Modifier.size(24.dp)) {
|
||||||
|
Icon(Icons.Default.Edit, contentDescription = "Editar", modifier = Modifier.size(18.dp))
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
IconButton(onClick = { onDelete(record) }, modifier = Modifier.size(24.dp)) {
|
||||||
|
Icon(Icons.Default.Delete, contentDescription = "Eliminar", modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
android.text.format.DateFormat.format("dd/MM/yyyy", record.date).toString(),
|
||||||
|
style = MaterialTheme.typography.labelSmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text("${record.liters} L x ${record.pricePerLiter} €/L", style = MaterialTheme.typography.bodyMedium)
|
||||||
|
Text(
|
||||||
|
"Total: ${record.totalPrice} €",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,17 +384,122 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
fun FuelLogDialog(
|
||||||
Text(
|
stationName: String,
|
||||||
text = "Hello $name!",
|
pricePerLiter: Double,
|
||||||
modifier = modifier
|
initialLiters: String = "",
|
||||||
|
initialTotal: String = "",
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: (Double, Double) -> Unit
|
||||||
|
) {
|
||||||
|
var liters by remember { mutableStateOf(initialLiters) }
|
||||||
|
var total by remember { mutableStateOf(initialTotal) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text("Registrar Repostaje") },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text(stationName, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = liters,
|
||||||
|
onValueChange = { liters = it },
|
||||||
|
label = { Text("Litros") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = total,
|
||||||
|
onValueChange = { total = it },
|
||||||
|
label = { Text("Precio Total (€)") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Button(onClick = {
|
||||||
|
val l = liters.toDoubleOrNull() ?: 0.0
|
||||||
|
val t = total.toDoubleOrNull() ?: 0.0
|
||||||
|
if (l > 0 && t > 0) onConfirm(l, t)
|
||||||
|
}) { Text("Guardar") }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) { Text("Cancelar") }
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GreetingPreview() {
|
fun GasStationItem(station: GasStationInfo, onClick: () -> Unit, onLogClick: () -> Unit, onFavoriteClick: () -> Unit) {
|
||||||
PruebaTheme {
|
Card(
|
||||||
Greeting("Android")
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 6.dp)
|
||||||
|
.clickable { onClick() },
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = "${station.name} - ${station.municipality}",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
IconButton(onClick = onFavoriteClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (station.isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
|
||||||
|
contentDescription = "Favorito",
|
||||||
|
tint = if (station.isFavorite) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = station.address,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${String.format(Locale.getDefault(), "%.1f", station.distance / 1000)} km",
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${station.price} €/L",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = onLogClick,
|
||||||
|
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp),
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text("Registrar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
package com.example.prueba
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Intent
|
||||||
|
import android.location.Geocoder
|
||||||
|
import android.location.Location
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.car.app.CarContext
|
||||||
|
import androidx.car.app.Screen
|
||||||
|
import androidx.car.app.model.*
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.example.prueba.data.GasStationInfo
|
||||||
|
import com.example.prueba.data.GasStationRepository
|
||||||
|
import com.example.prueba.utils.FavoriteStorage
|
||||||
|
import com.example.prueba.utils.FuelRecord
|
||||||
|
import com.example.prueba.utils.FuelStorage
|
||||||
|
import com.example.prueba.utils.LocationStorage
|
||||||
|
import com.google.android.gms.location.*
|
||||||
|
import com.google.android.gms.tasks.CancellationTokenSource
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class MainScreen(carContext: CarContext) : Screen(carContext) {
|
||||||
|
private var gasStations: List<GasStationInfo> = emptyList()
|
||||||
|
private var currentAddress: String? = null
|
||||||
|
private var isLoading = true
|
||||||
|
private val repository = GasStationRepository()
|
||||||
|
|
||||||
|
init {
|
||||||
|
fetchLocationAndGasStations()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateAddress(location: Location) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val geocoder = Geocoder(carContext, Locale.getDefault())
|
||||||
|
val addresses = geocoder.getFromLocation(location.latitude, location.longitude, 1)
|
||||||
|
if (!addresses.isNullOrEmpty()) {
|
||||||
|
val address = addresses[0]
|
||||||
|
val street = address.thoroughfare
|
||||||
|
val number = address.subThoroughfare
|
||||||
|
|
||||||
|
currentAddress = if (street != null && number != null) {
|
||||||
|
"$street, $number"
|
||||||
|
} else {
|
||||||
|
street ?: address.getAddressLine(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Silencioso
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
private fun fetchLocationAndGasStations(forceRefresh: Boolean = false) {
|
||||||
|
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(carContext)
|
||||||
|
|
||||||
|
// Cargar última ubicación guardada inmediatamente si no es un refresh forzado
|
||||||
|
if (!forceRefresh) {
|
||||||
|
LocationStorage.getLastLocation(carContext)?.let { lastLoc ->
|
||||||
|
lifecycleScope.launch {
|
||||||
|
updateAddress(lastLoc)
|
||||||
|
gasStations = repository.getCheapestAndNearest(carContext, lastLoc)
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 15000)
|
||||||
|
.setMinUpdateDistanceMeters(500f)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val locationCallback = object : LocationCallback() {
|
||||||
|
override fun onLocationResult(result: LocationResult) {
|
||||||
|
val location = result.lastLocation ?: return
|
||||||
|
LocationStorage.saveLocation(carContext, location)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
updateAddress(location)
|
||||||
|
gasStations = repository.getCheapestAndNearest(carContext, location, forceRefresh)
|
||||||
|
isLoading = false
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
// Si era un forceRefresh, después de la primera actualización volvemos a false
|
||||||
|
// para que los updates automáticos del GPS no sigan forzando red innecesariamente
|
||||||
|
if (forceRefresh) {
|
||||||
|
fusedLocationClient.removeLocationUpdates(this)
|
||||||
|
fetchLocationAndGasStations(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fusedLocationClient.requestLocationUpdates(
|
||||||
|
locationRequest,
|
||||||
|
locationCallback,
|
||||||
|
carContext.mainLooper
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGetTemplate(): Template {
|
||||||
|
val listBuilder = ItemList.Builder()
|
||||||
|
|
||||||
|
val refreshAction = Action.Builder()
|
||||||
|
.setTitle("Refrescar")
|
||||||
|
.setOnClickListener {
|
||||||
|
isLoading = true
|
||||||
|
invalidate()
|
||||||
|
fetchLocationAndGasStations(forceRefresh = true)
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
|
||||||
|
if (isLoading && gasStations.isEmpty()) {
|
||||||
|
return MessageTemplate.Builder("Buscando gasolineras...")
|
||||||
|
.setLoading(true)
|
||||||
|
.setHeader(
|
||||||
|
Header.Builder()
|
||||||
|
.setStartHeaderAction(Action.APP_ICON)
|
||||||
|
.setTitle("Gasolineras")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gasStations.isEmpty()) {
|
||||||
|
return MessageTemplate.Builder("No se encontraron gasolineras o no hay permiso de ubicación.")
|
||||||
|
.setHeader(
|
||||||
|
Header.Builder()
|
||||||
|
.setStartHeaderAction(Action.APP_ICON)
|
||||||
|
.setTitle("Gasolineras")
|
||||||
|
.addEndHeaderAction(refreshAction)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.addAction(
|
||||||
|
Action.Builder()
|
||||||
|
.setTitle("Reintentar")
|
||||||
|
.setOnClickListener {
|
||||||
|
isLoading = true
|
||||||
|
invalidate()
|
||||||
|
fetchLocationAndGasStations()
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (station in gasStations) {
|
||||||
|
val distanceStr = String.format(Locale.getDefault(), "%.1f km", station.distance / 1000)
|
||||||
|
listBuilder.addItem(
|
||||||
|
Row.Builder()
|
||||||
|
.setTitle("${station.name} - ${station.municipality}")
|
||||||
|
.addText("${station.price} €/L • $distanceStr")
|
||||||
|
.addText(station.address)
|
||||||
|
.setOnClickListener {
|
||||||
|
val pane = Pane.Builder()
|
||||||
|
.addRow(Row.Builder().setTitle("Precio").addText("${station.price} €/L").build())
|
||||||
|
.addRow(Row.Builder().setTitle("Distancia").addText(distanceStr).build())
|
||||||
|
.addAction(
|
||||||
|
Action.Builder()
|
||||||
|
.setTitle(if (station.isFavorite) "Quitar Favorito" else "Añadir Favorito")
|
||||||
|
.setOnClickListener {
|
||||||
|
FavoriteStorage.toggleFavorite(carContext, station.id)
|
||||||
|
screenManager.pop()
|
||||||
|
fetchLocationAndGasStations(forceRefresh = false)
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.addAction(
|
||||||
|
Action.Builder()
|
||||||
|
.setTitle("Ir ahora")
|
||||||
|
.setOnClickListener {
|
||||||
|
val uri = Uri.parse("geo:${station.latitude},${station.longitude}?q=${station.latitude},${station.longitude}")
|
||||||
|
val intent = Intent(CarContext.ACTION_NAVIGATE, uri)
|
||||||
|
carContext.startCarApp(intent)
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.addAction(
|
||||||
|
Action.Builder()
|
||||||
|
.setTitle("Registrar Repostaje")
|
||||||
|
.setOnClickListener {
|
||||||
|
// Guardar record simplificado desde el coche
|
||||||
|
val record = FuelRecord(
|
||||||
|
date = System.currentTimeMillis(),
|
||||||
|
stationName = station.name,
|
||||||
|
address = station.address,
|
||||||
|
municipality = station.municipality,
|
||||||
|
liters = 0.0, // Se editará en el móvil después
|
||||||
|
totalPrice = 0.0,
|
||||||
|
pricePerLiter = station.price
|
||||||
|
)
|
||||||
|
FuelStorage.saveRecord(carContext, record)
|
||||||
|
screenManager.pop()
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
screenManager.push(
|
||||||
|
object : Screen(carContext) {
|
||||||
|
override fun onGetTemplate(): Template {
|
||||||
|
return PaneTemplate.Builder(pane)
|
||||||
|
.setHeader(Header.Builder().setTitle(station.name).setStartHeaderAction(Action.BACK).build())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTemplate.Builder()
|
||||||
|
.setSingleList(listBuilder.build())
|
||||||
|
.setHeader(
|
||||||
|
Header.Builder()
|
||||||
|
.setStartHeaderAction(Action.APP_ICON)
|
||||||
|
.setTitle(currentAddress ?: "Gasolineras más baratas")
|
||||||
|
.addEndHeaderAction(refreshAction)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.example.prueba
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import androidx.car.app.CarAppService
|
||||||
|
import androidx.car.app.Session
|
||||||
|
import androidx.car.app.validation.HostValidator
|
||||||
|
|
||||||
|
class MyCarAppService : CarAppService() {
|
||||||
|
override fun createHostValidator(): HostValidator {
|
||||||
|
return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateSession(): Session {
|
||||||
|
return object : Session() {
|
||||||
|
override fun onCreateScreen(intent: Intent): androidx.car.app.Screen {
|
||||||
|
return MainScreen(carContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.example.prueba.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.location.Location
|
||||||
|
import com.example.prueba.utils.FavoriteStorage
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
|
||||||
|
class GasStationRepository {
|
||||||
|
private var cachedResponse: GasolineraResponse? = null
|
||||||
|
|
||||||
|
private val api: GasolineraApi = Retrofit.Builder()
|
||||||
|
.baseUrl("https://sedeaplicaciones.minetur.gob.es/ServiciosRESTCarburantes/PreciosCarburantes/")
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
.create(GasolineraApi::class.java)
|
||||||
|
|
||||||
|
suspend fun getCheapestAndNearest(context: Context, userLocation: Location, forceRefresh: Boolean = false): List<GasStationInfo> {
|
||||||
|
return try {
|
||||||
|
if (cachedResponse == null || forceRefresh) {
|
||||||
|
cachedResponse = api.getGasolineras()
|
||||||
|
}
|
||||||
|
|
||||||
|
val favorites = FavoriteStorage.getFavoriteIds(context)
|
||||||
|
|
||||||
|
cachedResponse?.listaEESS?.mapNotNull {
|
||||||
|
val lat = it.latitud.replace(",", ".").toDoubleOrNull()
|
||||||
|
val lon = it.longitud.replace(",", ".").toDoubleOrNull()
|
||||||
|
val price = it.precioDiesel.replace(",", ".").toDoubleOrNull() ?: it.precioGasolina95.replace(",", ".").toDoubleOrNull()
|
||||||
|
|
||||||
|
if (lat != null && lon != null && price != null) {
|
||||||
|
val stationLoc = Location("").apply {
|
||||||
|
latitude = lat
|
||||||
|
longitude = lon
|
||||||
|
}
|
||||||
|
val distance = userLocation.distanceTo(stationLoc)
|
||||||
|
GasStationInfo(
|
||||||
|
id = it.id,
|
||||||
|
name = it.rotulo,
|
||||||
|
address = it.direccion,
|
||||||
|
municipality = it.municipio,
|
||||||
|
price = price,
|
||||||
|
distance = distance,
|
||||||
|
latitude = lat,
|
||||||
|
longitude = lon,
|
||||||
|
isFavorite = favorites.contains(it.id)
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
}?.sortedBy { it.distance }
|
||||||
|
?.take(50)
|
||||||
|
?.sortedWith(compareByDescending<GasStationInfo> { it.isFavorite }.thenBy { it.price })
|
||||||
|
?.take(10) ?: emptyList()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class GasStationInfo(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val address: String,
|
||||||
|
val municipality: String,
|
||||||
|
val price: Double,
|
||||||
|
val distance: Float,
|
||||||
|
val latitude: Double,
|
||||||
|
val longitude: Double,
|
||||||
|
val isFavorite: Boolean = false
|
||||||
|
)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.example.prueba.data
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import retrofit2.http.GET
|
||||||
|
|
||||||
|
data class GasolineraResponse(
|
||||||
|
@SerializedName("ListaEESSPrecio")
|
||||||
|
val listaEESS: List<Gasolinera>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Gasolinera(
|
||||||
|
@SerializedName("IDEESS")
|
||||||
|
val id: String,
|
||||||
|
@SerializedName("Rótulo")
|
||||||
|
val rotulo: String,
|
||||||
|
@SerializedName("Dirección")
|
||||||
|
val direccion: String,
|
||||||
|
@SerializedName("Municipio")
|
||||||
|
val municipio: String,
|
||||||
|
@SerializedName("Precio Gasoleo A")
|
||||||
|
val precioDiesel: String,
|
||||||
|
@SerializedName("Precio Gasolina 95 E5")
|
||||||
|
val precioGasolina95: String,
|
||||||
|
@SerializedName("Latitud")
|
||||||
|
val latitud: String,
|
||||||
|
@SerializedName("Longitud (WGS84)")
|
||||||
|
val longitud: String
|
||||||
|
)
|
||||||
|
|
||||||
|
interface GasolineraApi {
|
||||||
|
@GET("EstacionesTerrestres/")
|
||||||
|
suspend fun getGasolineras(): GasolineraResponse
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.example.prueba.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
object FavoriteStorage {
|
||||||
|
private const val PREFS_NAME = "favorite_prefs"
|
||||||
|
private const val KEY_FAVORITES = "favorite_ids"
|
||||||
|
|
||||||
|
fun toggleFavorite(context: Context, stationId: String) {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
val favorites = getFavoriteIds(context).toMutableSet()
|
||||||
|
|
||||||
|
if (favorites.contains(stationId)) {
|
||||||
|
favorites.remove(stationId)
|
||||||
|
} else {
|
||||||
|
favorites.add(stationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit().putStringSet(KEY_FAVORITES, favorites).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFavoriteIds(context: Context): Set<String> {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
return prefs.getStringSet(KEY_FAVORITES, emptySet()) ?: emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isFavorite(context: Context, stationId: String): Boolean {
|
||||||
|
return getFavoriteIds(context).contains(stationId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.example.prueba.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
|
||||||
|
data class FuelRecord(
|
||||||
|
val date: Long,
|
||||||
|
val stationName: String,
|
||||||
|
val address: String,
|
||||||
|
val municipality: String,
|
||||||
|
val liters: Double,
|
||||||
|
val totalPrice: Double,
|
||||||
|
val pricePerLiter: Double
|
||||||
|
)
|
||||||
|
|
||||||
|
object FuelStorage {
|
||||||
|
private const val PREFS_NAME = "fuel_prefs"
|
||||||
|
private const val KEY_RECORDS = "fuel_records"
|
||||||
|
private val gson = Gson()
|
||||||
|
|
||||||
|
fun saveRecord(context: Context, record: FuelRecord) {
|
||||||
|
val records = getRecords(context).toMutableList()
|
||||||
|
records.add(0, record) // Añadir al principio
|
||||||
|
saveAllRecords(context, records)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRecords(context: Context): List<FuelRecord> {
|
||||||
|
val json = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
.getString(KEY_RECORDS, null) ?: return emptyList()
|
||||||
|
val type = object : TypeToken<List<FuelRecord>>() {}.type
|
||||||
|
return gson.fromJson(json, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteRecord(context: Context, record: FuelRecord) {
|
||||||
|
val records = getRecords(context).toMutableList()
|
||||||
|
records.remove(record)
|
||||||
|
saveAllRecords(context, records)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateRecord(context: Context, oldRecord: FuelRecord, newRecord: FuelRecord) {
|
||||||
|
val records = getRecords(context).toMutableList()
|
||||||
|
val index = records.indexOf(oldRecord)
|
||||||
|
if (index != -1) {
|
||||||
|
records[index] = newRecord
|
||||||
|
saveAllRecords(context, records)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveAllRecords(context: Context, records: List<FuelRecord>) {
|
||||||
|
val json = gson.toJson(records)
|
||||||
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putString(KEY_RECORDS, json)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.example.prueba.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.location.Location
|
||||||
|
|
||||||
|
object LocationStorage {
|
||||||
|
private const val PREFS_NAME = "location_prefs"
|
||||||
|
private const val KEY_LAT = "last_lat"
|
||||||
|
private const val KEY_LON = "last_lon"
|
||||||
|
|
||||||
|
fun saveLocation(context: Context, location: Location) {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
prefs.edit()
|
||||||
|
.putFloat(KEY_LAT, location.latitude.toFloat())
|
||||||
|
.putFloat(KEY_LON, location.longitude.toFloat())
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLastLocation(context: Context): Location? {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
val lat = prefs.getFloat(KEY_LAT, 0f)
|
||||||
|
val lon = prefs.getFloat(KEY_LON, 0f)
|
||||||
|
|
||||||
|
if (lat == 0f && lon == 0f) return null
|
||||||
|
|
||||||
|
return Location("stored").apply {
|
||||||
|
latitude = lat.toDouble()
|
||||||
|
longitude = lon.toDouble()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<automotiveApp>
|
||||||
|
<uses name="template" />
|
||||||
|
</automotiveApp>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#This file is generated by updateDaemonJvm
|
||||||
|
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/cf726b4a1c84b50457225f9bba6d7650/redirect
|
||||||
|
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/fa1e318c287360478e3c83a9a3ef1007/redirect
|
||||||
|
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/cf726b4a1c84b50457225f9bba6d7650/redirect
|
||||||
|
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/fa1e318c287360478e3c83a9a3ef1007/redirect
|
||||||
|
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/c2dd35c9d0aaf0ba6ad0791320f99dfc/redirect
|
||||||
|
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/e5810bd7fd1f8a586644409d395a7e55/redirect
|
||||||
|
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/cf726b4a1c84b50457225f9bba6d7650/redirect
|
||||||
|
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/fa1e318c287360478e3c83a9a3ef1007/redirect
|
||||||
|
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7b3c4877c0749019e6805bb61e421497/redirect
|
||||||
|
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/d76df094a9cbbabd3b08251f9e61444a/redirect
|
||||||
|
toolchainVersion=25
|
||||||
@@ -8,6 +8,9 @@ lifecycleRuntimeKtx = "2.11.0"
|
|||||||
activityCompose = "1.13.0"
|
activityCompose = "1.13.0"
|
||||||
kotlin = "2.2.10"
|
kotlin = "2.2.10"
|
||||||
composeBom = "2026.02.01"
|
composeBom = "2026.02.01"
|
||||||
|
androidx-car-app = "1.7.0"
|
||||||
|
retrofit = "2.9.0"
|
||||||
|
playServicesLocation = "21.3.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -24,6 +27,11 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
|
|||||||
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||||
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
|
androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||||
|
androidx-car-app = { group = "androidx.car.app", name = "app", version.ref = "androidx-car-app" }
|
||||||
|
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||||
|
retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
|
||||||
|
play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playServicesLocation" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user