Final commit
This commit is contained in:
4
.idea/gradle.xml
generated
4
.idea/gradle.xml
generated
@@ -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
6
.idea/vcs.xml
generated
Normal 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>
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -1,95 +1,158 @@
|
|||||||
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 {
|
||||||
var horarios by remember { mutableStateOf<List<ColoniaHorario>>(emptyList()) }
|
CamionBasuraTheme {
|
||||||
|
PantallaMain(repository)
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
horarios = repository.obtenerHorarios()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MaterialTheme {
|
private fun verificarPermisos() {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
val fineLocation = ContextCompat.checkSelfPermission(
|
||||||
Text(
|
this, Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
text = "🚛 Camión de Basura - Celaya",
|
)
|
||||||
style = MaterialTheme.typography.headlineSmall
|
if (fineLocation == PackageManager.PERMISSION_GRANTED) {
|
||||||
)
|
iniciarServicio()
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
} else {
|
||||||
Text(
|
permisosLauncher.launch(
|
||||||
text = "Horarios por colonia:",
|
arrayOf(
|
||||||
style = MaterialTheme.typography.titleMedium
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
)
|
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
)
|
||||||
LazyColumn {
|
)
|
||||||
items(horarios) { horario ->
|
}
|
||||||
Card(
|
}
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
private fun iniciarServicio() {
|
||||||
.padding(vertical = 4.dp)
|
val intent = Intent(this, CamionMonitorService::class.java)
|
||||||
) {
|
startForegroundService(intent)
|
||||||
Column(modifier = Modifier.padding(12.dp)) {
|
}
|
||||||
Text(
|
}
|
||||||
text = horario.colonia,
|
|
||||||
style = MaterialTheme.typography.titleSmall
|
@Composable
|
||||||
)
|
fun PantallaMain(repository: CamionRepository) {
|
||||||
Text(text = "Ruta: ${horario.routeId}")
|
var horarios by remember { mutableStateOf<List<ColoniaHorario>>(emptyList()) }
|
||||||
Text(text = "Horario: ${horario.horarioEstimado}")
|
var rutas by remember { mutableStateOf<List<Ruta>>(emptyList()) }
|
||||||
}
|
var tabSeleccionado by remember { mutableStateOf(0) }
|
||||||
}
|
|
||||||
}
|
LaunchedEffect(Unit) {
|
||||||
}
|
withContext(Dispatchers.IO) {
|
||||||
}
|
horarios = repository.obtenerHorarios()
|
||||||
|
rutas = repository.obtenerRutas()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Text(
|
||||||
|
text = "🚛 Camión de Basura - Celaya",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
TabRow(selectedTabIndex = tabSeleccionado) {
|
||||||
|
Tab(selected = tabSeleccionado == 0, onClick = { tabSeleccionado = 0 }) {
|
||||||
|
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}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
fun ListaHorarios(horarios: List<ColoniaHorario>) {
|
||||||
Text(
|
LazyColumn(modifier = Modifier.padding(16.dp)) {
|
||||||
text = "Hello $name!",
|
items(horarios) { horario ->
|
||||||
modifier = modifier
|
Card(
|
||||||
)
|
modifier = Modifier
|
||||||
}
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
@Preview(showBackground = true)
|
) {
|
||||||
@Composable
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
fun GreetingPreview() {
|
Text(
|
||||||
CamionBasuraTheme {
|
text = horario.colonia,
|
||||||
Greeting("Android")
|
style = MaterialTheme.typography.titleSmall
|
||||||
|
)
|
||||||
|
Text(text = "Ruta: ${horario.routeId}")
|
||||||
|
Text(text = "Horario: ${horario.horarioEstimado}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
@@ -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") {
|
|
||||||
enviarNotificacion(
|
rutas.forEach { ruta ->
|
||||||
notificacion.pushPayload.title,
|
ruta.ubicacionActual?.let { ubicacionCamion ->
|
||||||
notificacion.pushPayload.body
|
val locationCamion = Location("camion").apply {
|
||||||
)
|
latitude = ubicacionCamion.lat
|
||||||
return
|
longitude = ubicacionCamion.lng
|
||||||
|
}
|
||||||
|
|
||||||
|
val distancia = miUbicacion.distanceTo(locationCamion)
|
||||||
|
|
||||||
|
if (distancia <= DISTANCIA_ALERTA_METROS) {
|
||||||
|
enviarNotificacion(
|
||||||
|
"🚛 ¡El camión está cerca!",
|
||||||
|
"El camión de la ruta ${ruta.routeId} está a ${distancia.toInt()}m de ti"
|
||||||
|
)
|
||||||
|
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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user