Inicio de aplicacion

This commit is contained in:
2026-08-10 12:15:45 +02:00
parent ccb900c453
commit 3d839131bd
17 changed files with 1027 additions and 21 deletions
@@ -1,29 +1,382 @@
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 androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
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.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.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() {
private val repository = GasStationRepository()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
PruebaTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
GasStationApp(repository)
}
}
}
}
@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
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
fun FuelLogDialog(
stationName: String,
pricePerLiter: Double,
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
fun GreetingPreview() {
PruebaTheme {
Greeting("Android")
fun GasStationItem(station: GasStationInfo, onClick: () -> Unit, onLogClick: () -> Unit, onFavoriteClick: () -> Unit) {
Card(
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")
}
}
}
}
}
}