Final commit

This commit is contained in:
Angel Osvaldo Frias Arriaga
2026-05-23 09:41:16 -06:00
parent cf9bdba59b
commit d0b7001137
7 changed files with 200 additions and 78 deletions

4
.idea/gradle.xml generated
View File

@@ -1,9 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<<<<<<< HEAD
<component name="GradleMigrationSettings" migrationVersion="1" />
=======
>>>>>>> a80ea289fd32cc25c1aadbfa295b6f00b7b775f6
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -60,5 +60,7 @@ dependencies {
implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.google.android.gms:play-services-maps:18.1.0") implementation("com.google.android.gms:play-services-maps:18.1.0")
implementation("com.google.maps.android:maps-compose:4.3.3")
implementation("com.google.android.gms:play-services-maps:18.2.0")
implementation("com.google.android.gms:play-services-location:21.2.0")
} }

View File

@@ -6,9 +6,10 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@@ -22,6 +23,7 @@
<meta-data <meta-data
android:name="com.google.geo.API_KEY" android:name="com.google.geo.API_KEY"
android:value="AIzaSyAg5e7AeqVs4kWEsHgdYr6mGjJiHvZc0hg"/> android:value="AIzaSyAg5e7AeqVs4kWEsHgdYr6mGjJiHvZc0hg"/>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -29,14 +31,14 @@
android:theme="@style/Theme.CamionBasura"> android:theme="@style/Theme.CamionBasura">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<category android:name="android.intent.category.LAUNCHER" />
</application>
<service <service
android:name=".service.CamionMonitorService" android:name=".service.CamionMonitorService"
android:foregroundServiceType="location" /> android:foregroundServiceType="location" />
</application>
</manifest> </manifest>

View File

@@ -1,59 +1,143 @@
package com.tuapp.camionbasura package com.tuapp.camionbasura
import android.Manifest
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.compose.*
import com.tuapp.camionbasura.model.ColoniaHorario import com.tuapp.camionbasura.model.ColoniaHorario
import com.tuapp.camionbasura.model.Ruta
import com.tuapp.camionbasura.network.CamionRepository import com.tuapp.camionbasura.network.CamionRepository
import com.tuapp.camionbasura.service.CamionMonitorService import com.tuapp.camionbasura.service.CamionMonitorService
import com.tuapp.camionbasura.ui.theme.CamionBasuraTheme import com.tuapp.camionbasura.ui.theme.CamionBasuraTheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.withContext
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val repository = CamionRepository() private val repository = CamionRepository()
// Solicitar permisos de ubicación
private val permisosLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permisos ->
if (permisos[Manifest.permission.ACCESS_FINE_LOCATION] == true) {
iniciarServicio()
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Iniciar servicio de monitoreo verificarPermisos()
val intent = Intent(this, CamionMonitorService::class.java)
startService(intent)
setContent { setContent {
CamionBasuraTheme {
PantallaMain(repository)
}
}
}
private fun verificarPermisos() {
val fineLocation = ContextCompat.checkSelfPermission(
this, Manifest.permission.ACCESS_FINE_LOCATION
)
if (fineLocation == PackageManager.PERMISSION_GRANTED) {
iniciarServicio()
} else {
permisosLauncher.launch(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
}
}
private fun iniciarServicio() {
val intent = Intent(this, CamionMonitorService::class.java)
startForegroundService(intent)
}
}
@Composable
fun PantallaMain(repository: CamionRepository) {
var horarios by remember { mutableStateOf<List<ColoniaHorario>>(emptyList()) } var horarios by remember { mutableStateOf<List<ColoniaHorario>>(emptyList()) }
var rutas by remember { mutableStateOf<List<Ruta>>(emptyList()) }
var tabSeleccionado by remember { mutableStateOf(0) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
CoroutineScope(Dispatchers.IO).launch { withContext(Dispatchers.IO) {
horarios = repository.obtenerHorarios() horarios = repository.obtenerHorarios()
rutas = repository.obtenerRutas()
} }
} }
MaterialTheme { Column(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.padding(16.dp)) {
Text( Text(
text = "🚛 Camión de Basura - Celaya", text = "🚛 Camión de Basura - Celaya",
style = MaterialTheme.typography.headlineSmall style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(16.dp)
) )
Spacer(modifier = Modifier.height(16.dp))
Text( TabRow(selectedTabIndex = tabSeleccionado) {
text = "Horarios por colonia:", Tab(selected = tabSeleccionado == 0, onClick = { tabSeleccionado = 0 }) {
style = MaterialTheme.typography.titleMedium Text("Mapa", modifier = Modifier.padding(12.dp))
}
Tab(selected = tabSeleccionado == 1, onClick = { tabSeleccionado = 1 }) {
Text("Horarios", modifier = Modifier.padding(12.dp))
}
}
when (tabSeleccionado) {
0 -> MapaCamion(rutas)
1 -> ListaHorarios(horarios)
}
}
}
@Composable
fun MapaCamion(rutas: List<Ruta>) {
// Centro de Celaya
val celaya = LatLng(20.5236, -100.8152)
val cameraState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(celaya, 13f)
}
GoogleMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = cameraState,
properties = MapProperties(isMyLocationEnabled = true)
) {
rutas.forEach { ruta ->
ruta.ubicacionActual?.let { ubicacion ->
val posicion = LatLng(ubicacion.lat, ubicacion.lng)
Marker(
state = MarkerState(position = posicion),
title = "Camión Ruta ${ruta.routeId}",
snippet = "Velocidad: ${ubicacion.speed} km/h • ${ubicacion.timestamp}"
) )
Spacer(modifier = Modifier.height(8.dp)) }
LazyColumn { }
}
}
@Composable
fun ListaHorarios(horarios: List<ColoniaHorario>) {
LazyColumn(modifier = Modifier.padding(16.dp)) {
items(horarios) { horario -> items(horarios) { horario ->
Card( Card(
modifier = Modifier modifier = Modifier
@@ -71,25 +155,4 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
CamionBasuraTheme {
Greeting("Android")
}
} }

View File

@@ -13,4 +13,8 @@ data class Ruta(
val name: String, val name: String,
val status: String, val status: String,
val positions: List<Posicion> val positions: List<Posicion>
) ) {
// Obtiene la posicion mas reciente de la lista
val ubicacionActual: Posicion?
get() = positions.maxByOrNull { it.posicion }
}

View File

@@ -5,8 +5,10 @@ import android.app.NotificationManager
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.location.Location
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.google.android.gms.location.*
import com.tuapp.camionbasura.network.CamionRepository import com.tuapp.camionbasura.network.CamionRepository
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -15,31 +17,79 @@ class CamionMonitorService : Service() {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val repository = CamionRepository() private val repository = CamionRepository()
private val CHANNEL_ID = "camion_channel" private val CHANNEL_ID = "camion_channel"
private val DISTANCIA_ALERTA_METROS = 500f // Notifica si el camión está a menos de 500m
private lateinit var fusedLocationClient: FusedLocationProviderClient
private var ubicacionUsuario: Location? = null
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
crearCanalNotificacion() crearCanalNotificacion()
iniciarComoForeground()
iniciarUbicacion()
iniciarMonitoreo() iniciarMonitoreo()
} }
// El servicio necesita una notificacion base para correr en foreground
private fun iniciarComoForeground() {
val notificacion = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Monitoreando camión de basura")
.setContentText("Recibirás una alerta cuando esté cerca")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
startForeground(99, notificacion)
}
private fun iniciarUbicacion() {
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
val request = LocationRequest.Builder(
Priority.PRIORITY_HIGH_ACCURACY, 15_000L
).build()
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
ubicacionUsuario = result.lastLocation
}
}
try {
fusedLocationClient.requestLocationUpdates(request, callback, mainLooper)
} catch (e: SecurityException) {
// Permiso no concedido
}
}
private fun iniciarMonitoreo() { private fun iniciarMonitoreo() {
scope.launch { scope.launch {
while (isActive) { while (isActive) {
verificarNotificaciones() verificarProximidad()
delay(30_000L) delay(30_000L)
} }
} }
} }
private suspend fun verificarNotificaciones() { private suspend fun verificarProximidad() {
val notificaciones = repository.obtenerNotificaciones() val rutas = repository.obtenerRutas()
notificaciones.forEach { notificacion -> val miUbicacion = ubicacionUsuario ?: return
if (notificacion.triggerEvent == "TRUCK_PROXIMITY") {
rutas.forEach { ruta ->
ruta.ubicacionActual?.let { ubicacionCamion ->
val locationCamion = Location("camion").apply {
latitude = ubicacionCamion.lat
longitude = ubicacionCamion.lng
}
val distancia = miUbicacion.distanceTo(locationCamion)
if (distancia <= DISTANCIA_ALERTA_METROS) {
enviarNotificacion( enviarNotificacion(
notificacion.pushPayload.title, "🚛 ¡El camión está cerca!",
notificacion.pushPayload.body "El camión de la ruta ${ruta.routeId} está a ${distancia.toInt()}m de ti"
) )
return return // Una notificacion es suficiente
}
} }
} }
} }
@@ -64,7 +114,6 @@ class CamionMonitorService : Service() {
.build() .build()
manager.notify(1, notificacion) manager.notify(1, notificacion)
} }
override fun onBind(intent: Intent?): IBinder? = null override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() { override fun onDestroy() {