Ya casiii
This commit is contained in:
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceTable">
|
||||
<option name="columnSorters">
|
||||
<list>
|
||||
<ColumnSorterState>
|
||||
<option name="column" value="Name" />
|
||||
<option name="order" value="ASCENDING" />
|
||||
</ColumnSorterState>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -37,21 +37,32 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// AndroidX (versiones compatibles con SDK 34)
|
||||
// Forzar versiones compatibles con SDK 34
|
||||
implementation("androidx.core:core:1.12.0")
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.activity:activity:1.8.2")
|
||||
implementation("androidx.activity:activity-ktx:1.8.2")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("com.google.android.material:material:1.11.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("androidx.activity:activity-ktx:1.8.2")
|
||||
implementation("androidx.fragment:fragment-ktx:1.6.2")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||
implementation("androidx.drawerlayout:drawerlayout:1.2.0")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|
||||
|
||||
// Firebase
|
||||
// Firebase (BoM antigua compatible)
|
||||
implementation(platform("com.google.firebase:firebase-bom:32.7.4"))
|
||||
implementation("com.google.firebase:firebase-auth-ktx")
|
||||
implementation("com.google.firebase:firebase-firestore-ktx")
|
||||
|
||||
// Retrofit + OkHttp (versiones estables)
|
||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
|
||||
|
||||
// Coroutines
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
|
||||
@@ -59,4 +70,14 @@ dependencies {
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
}
|
||||
|
||||
// Forzar versiones específicas para evitar conflictos transitivos
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
force("androidx.core:core:1.12.0")
|
||||
force("androidx.core:core-ktx:1.12.0")
|
||||
force("androidx.activity:activity:1.8.2")
|
||||
force("androidx.activity:activity-ktx:1.8.2")
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -15,6 +16,9 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.BasurApp"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".AddAddressActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".RegisterActivity"
|
||||
android:exported="false" />
|
||||
@@ -30,6 +34,9 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".AddAddressActivity"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
94
app/src/main/java/com/example/basurapp/AddAddressActivity.kt
Normal file
94
app/src/main/java/com/example/basurapp/AddAddressActivity.kt
Normal file
@@ -0,0 +1,94 @@
|
||||
package com.example.basurapp
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.example.basurapp.api.ApiClient
|
||||
import com.example.basurapp.databinding.ActivityAddAddressBinding
|
||||
import com.example.basurapp.models.Address
|
||||
import com.example.basurapp.models.ColoniaRuta
|
||||
import com.google.firebase.auth.FirebaseAuth
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AddAddressActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityAddAddressBinding
|
||||
private val auth: FirebaseAuth by lazy { FirebaseAuth.getInstance() }
|
||||
private val db: FirebaseFirestore by lazy { FirebaseFirestore.getInstance() }
|
||||
|
||||
private var coloniasList: List<ColoniaRuta> = emptyList()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityAddAddressBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
loadColonias()
|
||||
|
||||
binding.btnSave.setOnClickListener {
|
||||
saveAddress()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadColonias() {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
coloniasList = ApiClient.api.getColoniasRutas()
|
||||
val names = coloniasList.map { it.colonia }
|
||||
val adapter = ArrayAdapter(this@AddAddressActivity, android.R.layout.simple_dropdown_item_1line, names)
|
||||
binding.actvColonia.setAdapter(adapter)
|
||||
binding.progressBar.visibility = View.GONE
|
||||
} catch (e: Exception) {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
Toast.makeText(this@AddAddressActivity, "Error cargando colonias: ${e.localizedMessage}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAddress() {
|
||||
val street = binding.etStreet.text?.toString()?.trim().orEmpty()
|
||||
val number = binding.etNumber.text?.toString()?.trim().orEmpty()
|
||||
val coloniaSelected = binding.actvColonia.text?.toString()?.trim().orEmpty()
|
||||
val reference = binding.etReference.text?.toString()?.trim().orEmpty()
|
||||
|
||||
if (street.isEmpty() || number.isEmpty() || coloniaSelected.isEmpty()) {
|
||||
Toast.makeText(this, "Completa calle, número y colonia", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
// Validar que la colonia exista en el catálogo
|
||||
val validColonia = coloniasList.any { it.colonia.equals(coloniaSelected, ignoreCase = true) }
|
||||
if (!validColonia) {
|
||||
Toast.makeText(this, "Selecciona una colonia válida de la lista", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val uid = auth.currentUser?.uid ?: return
|
||||
val addressId = db.collection("addresses").document().id
|
||||
val address = Address(
|
||||
id = addressId,
|
||||
userId = uid,
|
||||
street = street,
|
||||
number = number,
|
||||
neighborhood = coloniaSelected,
|
||||
city = "Celaya",
|
||||
reference = reference
|
||||
)
|
||||
|
||||
binding.btnSave.isEnabled = false
|
||||
db.collection("addresses").document(addressId).set(address)
|
||||
.addOnSuccessListener {
|
||||
Toast.makeText(this, "Dirección guardada", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
binding.btnSave.isEnabled = true
|
||||
Toast.makeText(this, e.localizedMessage ?: "Error", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,10 +31,10 @@ class LoginActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
// Si ya hay sesión activa, ir directo al Main
|
||||
if (auth.currentUser != null) {
|
||||
goToMain()
|
||||
return
|
||||
}
|
||||
// if (auth.currentUser != null) {
|
||||
// goToMain()
|
||||
// return
|
||||
// }
|
||||
|
||||
binding.tvCreateAccount.setOnClickListener {
|
||||
startActivity(Intent(this, RegisterActivity::class.java))
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package com.example.basurapp
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.example.basurapp.databinding.ActivityMainBinding
|
||||
@@ -50,6 +54,20 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
||||
binding.navigationView.setNavigationItemSelectedListener(this)
|
||||
|
||||
loadCurrentUser()
|
||||
|
||||
// Solicitar permiso de notificaciones en Android 13+
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this, android.Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
|
||||
100
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCurrentUser() {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.example.basurapp.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.example.basurapp.databinding.ItemAddressBinding
|
||||
import com.example.basurapp.models.Address
|
||||
|
||||
class AddressesAdapter(
|
||||
private val onDelete: (Address) -> Unit
|
||||
) : ListAdapter<Address, AddressesAdapter.AddressViewHolder>(DIFF) {
|
||||
|
||||
inner class AddressViewHolder(val binding: ItemAddressBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(address: Address) {
|
||||
binding.tvStreet.text = "${address.street} ${address.number}"
|
||||
binding.tvNeighborhood.text = address.neighborhood
|
||||
if (address.reference.isNotEmpty()) {
|
||||
binding.tvReference.text = address.reference
|
||||
binding.tvReference.visibility = android.view.View.VISIBLE
|
||||
} else {
|
||||
binding.tvReference.visibility = android.view.View.GONE
|
||||
}
|
||||
binding.btnDelete.setOnClickListener { onDelete(address) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddressViewHolder {
|
||||
val binding = ItemAddressBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
return AddressViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: AddressViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DIFF = object : DiffUtil.ItemCallback<Address>() {
|
||||
override fun areItemsTheSame(oldItem: Address, newItem: Address) = oldItem.id == newItem.id
|
||||
override fun areContentsTheSame(oldItem: Address, newItem: Address) = oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
35
app/src/main/java/com/example/basurapp/api/ApiClient.kt
Normal file
35
app/src/main/java/com/example/basurapp/api/ApiClient.kt
Normal file
@@ -0,0 +1,35 @@
|
||||
package com.example.basurapp.api
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object ApiClient {
|
||||
|
||||
private const val BASE_URL = "https://td.celaya.biz:8018/"
|
||||
|
||||
private val okHttp by lazy {
|
||||
val logging = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BASIC
|
||||
}
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor(logging)
|
||||
.connectTimeout(20, TimeUnit.SECONDS)
|
||||
.readTimeout(20, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
private val retrofit by lazy {
|
||||
Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(okHttp)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
}
|
||||
|
||||
val api: BasurAppApi by lazy {
|
||||
retrofit.create(BasurAppApi::class.java)
|
||||
}
|
||||
}
|
||||
18
app/src/main/java/com/example/basurapp/api/BasurAppApi.kt
Normal file
18
app/src/main/java/com/example/basurapp/api/BasurAppApi.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.example.basurapp.api
|
||||
|
||||
import com.example.basurapp.models.ApiRoute
|
||||
import com.example.basurapp.models.ColoniaRuta
|
||||
import com.example.basurapp.models.NotificationRule
|
||||
import retrofit2.http.GET
|
||||
|
||||
interface BasurAppApi {
|
||||
|
||||
@GET("sistemasJSON/rutas.json")
|
||||
suspend fun getRoutes(): List<ApiRoute>
|
||||
|
||||
@GET("sistemasJSON/colonias-rutas.json")
|
||||
suspend fun getColoniasRutas(): List<ColoniaRuta>
|
||||
|
||||
@GET("sistemasJSON/notificaciones.json")
|
||||
suspend fun getNotificationRules(): List<NotificationRule>
|
||||
}
|
||||
@@ -1,34 +1,67 @@
|
||||
package com.example.basurapp.fragments
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.example.basurapp.AddAddressActivity
|
||||
import com.example.basurapp.adapters.AddressesAdapter
|
||||
import com.example.basurapp.adapters.RoutesAdapter
|
||||
import com.example.basurapp.databinding.HomeFragmentBinding
|
||||
import com.example.basurapp.api.ApiClient
|
||||
import com.example.basurapp.databinding.FragmentHomeBinding
|
||||
import com.example.basurapp.dialogs.RouteInfoDialog
|
||||
import com.example.basurapp.models.ApiRoute
|
||||
import com.example.basurapp.models.Address
|
||||
import com.example.basurapp.models.ColoniaRuta
|
||||
import com.example.basurapp.models.NotificationRule
|
||||
import com.example.basurapp.models.Route
|
||||
import com.example.basurapp.models.RouteStop
|
||||
import com.example.basurapp.utils.NotificationHelper
|
||||
import com.example.basurapp.utils.RouteTracker
|
||||
import com.google.firebase.auth.FirebaseAuth
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class HomeFragment : Fragment() {
|
||||
|
||||
private var _binding: HomeFragmentBinding? = null
|
||||
private var _binding: FragmentHomeBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val auth: FirebaseAuth by lazy { FirebaseAuth.getInstance() }
|
||||
private val db: FirebaseFirestore by lazy { FirebaseFirestore.getInstance() }
|
||||
|
||||
private lateinit var routesAdapter: RoutesAdapter
|
||||
private lateinit var addressesAdapter: AddressesAdapter
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val POLL_INTERVAL_MS = 2_000L // 2 segundos
|
||||
|
||||
private var userAddresses: List<Address> = emptyList()
|
||||
private var coloniasRutas: List<ColoniaRuta> = emptyList()
|
||||
private var notificationRules: List<NotificationRule> = emptyList()
|
||||
|
||||
// Mapa: routeId -> último positionId conocido (para detectar cambios por ruta)
|
||||
private val lastPositionByRoute = mutableMapOf<String, Int>()
|
||||
|
||||
private val pollRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
refreshRoutes()
|
||||
handler.postDelayed(this, POLL_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = HomeFragmentBinding.inflate(inflater, container, false)
|
||||
_binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@@ -42,24 +75,50 @@ class HomeFragment : Fragment() {
|
||||
binding.rvRoutes.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.rvRoutes.adapter = routesAdapter
|
||||
|
||||
binding.btnAddAddress.setOnClickListener {
|
||||
Toast.makeText(requireContext(), "Función Agregar dirección (pendiente)", Toast.LENGTH_SHORT).show()
|
||||
// TODO: Abrir Activity o dialog para agregar dirección
|
||||
// Adapter de direcciones
|
||||
addressesAdapter = AddressesAdapter { address ->
|
||||
confirmDeleteAddress(address)
|
||||
}
|
||||
binding.rvAddresses.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.rvAddresses.adapter = addressesAdapter
|
||||
|
||||
// Botones para agregar dirección
|
||||
binding.btnAddAddressEmpty.setOnClickListener {
|
||||
startActivity(Intent(requireContext(), AddAddressActivity::class.java))
|
||||
}
|
||||
binding.btnAddMoreAddress.setOnClickListener {
|
||||
startActivity(Intent(requireContext(), AddAddressActivity::class.java))
|
||||
}
|
||||
|
||||
checkUserAddress()
|
||||
NotificationHelper.ensureChannel(requireContext())
|
||||
loadNotificationRules()
|
||||
}
|
||||
|
||||
private fun checkUserAddress() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Recargar direcciones cada vez que vuelve al fragment (después de agregar)
|
||||
loadUserAddresses()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
stopPolling()
|
||||
}
|
||||
|
||||
private fun loadUserAddresses() {
|
||||
val uid = auth.currentUser?.uid ?: return
|
||||
db.collection("addresses")
|
||||
.whereEqualTo("userId", uid)
|
||||
.get()
|
||||
.addOnSuccessListener { result ->
|
||||
if (result.isEmpty) {
|
||||
userAddresses = result.documents.mapNotNull { it.toObject(Address::class.java) }
|
||||
if (userAddresses.isEmpty()) {
|
||||
showNoAddressState()
|
||||
stopPolling()
|
||||
} else {
|
||||
showRoutesState()
|
||||
showWithAddressesState()
|
||||
addressesAdapter.submitList(userAddresses)
|
||||
startPolling()
|
||||
}
|
||||
}
|
||||
.addOnFailureListener {
|
||||
@@ -67,51 +126,177 @@ class HomeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun confirmDeleteAddress(address: Address) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle("Eliminar dirección")
|
||||
.setMessage("¿Quieres eliminar ${address.street} ${address.number}?")
|
||||
.setPositiveButton("Eliminar") { _, _ ->
|
||||
deleteAddress(address)
|
||||
}
|
||||
.setNegativeButton("Cancelar", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun deleteAddress(address: Address) {
|
||||
db.collection("addresses").document(address.id).delete()
|
||||
.addOnSuccessListener {
|
||||
Toast.makeText(requireContext(), "Dirección eliminada", Toast.LENGTH_SHORT).show()
|
||||
loadUserAddresses()
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Toast.makeText(requireContext(), e.localizedMessage ?: "Error", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNoAddressState() {
|
||||
if (_binding == null) return
|
||||
binding.layoutNoAddress.visibility = View.VISIBLE
|
||||
binding.rvRoutes.visibility = View.GONE
|
||||
binding.layoutWithAddresses.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun showRoutesState() {
|
||||
private fun showWithAddressesState() {
|
||||
if (_binding == null) return
|
||||
binding.layoutNoAddress.visibility = View.GONE
|
||||
binding.rvRoutes.visibility = View.VISIBLE
|
||||
loadRoutes()
|
||||
binding.layoutWithAddresses.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun loadRoutes() {
|
||||
// Por ahora datos de ejemplo (después se conectan a Firestore)
|
||||
val sampleRoutes = listOf(
|
||||
Route(
|
||||
id = "1",
|
||||
name = "Dirección 1",
|
||||
neighborhood = "Centro",
|
||||
stops = listOf(
|
||||
RouteStop("Inicio", "07:00"),
|
||||
RouteStop("Calle 5 de Mayo", "07:30"),
|
||||
RouteStop("Plaza Central", "08:00"),
|
||||
RouteStop("Av. Juárez", "08:30"),
|
||||
RouteStop("Final de ruta", "09:00")
|
||||
),
|
||||
truckPosition = 2
|
||||
),
|
||||
Route(
|
||||
id = "2",
|
||||
name = "Dirección 1",
|
||||
neighborhood = "Zona Norte",
|
||||
stops = listOf(
|
||||
RouteStop("Inicio", "10:00"),
|
||||
RouteStop("Mercado Norte", "10:30"),
|
||||
RouteStop("Parque Hidalgo", "11:00"),
|
||||
RouteStop("Final de ruta", "11:30")
|
||||
),
|
||||
truckPosition = 1
|
||||
)
|
||||
private fun startPolling() {
|
||||
stopPolling()
|
||||
handler.post(pollRunnable)
|
||||
}
|
||||
|
||||
private fun stopPolling() {
|
||||
handler.removeCallbacks(pollRunnable)
|
||||
}
|
||||
|
||||
private fun loadNotificationRules() {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
notificationRules = ApiClient.api.getNotificationRules()
|
||||
} catch (e: Exception) {
|
||||
// silenciar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshRoutes() {
|
||||
if (userAddresses.isEmpty()) return
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
if (coloniasRutas.isEmpty()) {
|
||||
coloniasRutas = ApiClient.api.getColoniasRutas()
|
||||
}
|
||||
|
||||
val allRoutes = ApiClient.api.getRoutes()
|
||||
|
||||
// Para cada dirección del usuario, encontrar su ruta correspondiente
|
||||
val matchedRoutes = mutableListOf<Pair<ApiRoute, ColoniaRuta>>()
|
||||
val seenRouteIds = mutableSetOf<String>()
|
||||
|
||||
for (address in userAddresses) {
|
||||
val coloniaInfo = coloniasRutas.firstOrNull {
|
||||
it.colonia.equals(address.neighborhood, ignoreCase = true)
|
||||
} ?: continue
|
||||
|
||||
// Evitar mostrar la misma ruta dos veces si dos direcciones comparten ruta
|
||||
if (seenRouteIds.contains(coloniaInfo.routeId)) continue
|
||||
seenRouteIds.add(coloniaInfo.routeId)
|
||||
|
||||
val apiRoute = allRoutes.firstOrNull { it.routeId == coloniaInfo.routeId }
|
||||
?: continue
|
||||
|
||||
matchedRoutes.add(apiRoute to coloniaInfo)
|
||||
|
||||
// Checar cambio de posición para notificaciones
|
||||
checkPositionChange(apiRoute, address)
|
||||
}
|
||||
|
||||
// Convertir a modelo visual
|
||||
val visualRoutes = matchedRoutes.map { (apiRoute, colonia) ->
|
||||
mapToVisualRoute(apiRoute, colonia)
|
||||
}
|
||||
routesAdapter.submitList(visualRoutes) {
|
||||
// Después de actualizar, forzar redibujado para que se vea el movimiento del camión
|
||||
routesAdapter.notifyDataSetChanged()
|
||||
}
|
||||
routesAdapter.submitList(visualRoutes)
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPositionChange(apiRoute: ApiRoute, address: Address) {
|
||||
val currentId = RouteTracker.getCurrentPositionId(apiRoute)
|
||||
val lastId = lastPositionByRoute[apiRoute.routeId]
|
||||
|
||||
if (lastId == null) {
|
||||
lastPositionByRoute[apiRoute.routeId] = currentId
|
||||
return
|
||||
}
|
||||
|
||||
if (currentId == lastId) return
|
||||
|
||||
val totalPositions = apiRoute.positions.size
|
||||
val addressLabel = "${address.street} ${address.number}"
|
||||
|
||||
when {
|
||||
lastId == 1 && currentId >= 2 -> {
|
||||
fireNotification(NotificationRule.EVENT_ROUTE_START, apiRoute.routeId, addressLabel)
|
||||
}
|
||||
lastId < 4 && currentId >= 4 -> {
|
||||
fireNotification(NotificationRule.EVENT_TRUCK_PROXIMITY, apiRoute.routeId, addressLabel)
|
||||
}
|
||||
lastId < totalPositions && currentId >= totalPositions -> {
|
||||
fireNotification(NotificationRule.EVENT_ROUTE_COMPLETED, apiRoute.routeId, addressLabel)
|
||||
}
|
||||
}
|
||||
|
||||
lastPositionByRoute[apiRoute.routeId] = currentId
|
||||
}
|
||||
|
||||
private fun fireNotification(triggerEvent: String, routeId: String, addressLabel: String) {
|
||||
val rule = notificationRules.firstOrNull { it.triggerEvent == triggerEvent } ?: return
|
||||
val notificationId = (routeId.hashCode() + triggerEvent.hashCode())
|
||||
NotificationHelper.show(
|
||||
requireContext(),
|
||||
notificationId,
|
||||
rule.pushPayload.title,
|
||||
"${rule.pushPayload.body}\nDirección: $addressLabel"
|
||||
)
|
||||
routesAdapter.submitList(sampleRoutes)
|
||||
}
|
||||
|
||||
private fun mapToVisualRoute(apiRoute: ApiRoute, colonia: ColoniaRuta): Route {
|
||||
val stops = apiRoute.positions.map { pos ->
|
||||
RouteStop(
|
||||
name = "Punto ${pos.positionId}",
|
||||
time = extractTime(pos.timestamp)
|
||||
)
|
||||
}
|
||||
val currentId = RouteTracker.getCurrentPositionId(apiRoute)
|
||||
|
||||
return Route(
|
||||
id = apiRoute.routeId,
|
||||
name = "${colonia.colonia} - ${apiRoute.name}",
|
||||
neighborhood = colonia.colonia,
|
||||
stops = stops,
|
||||
truckPosition = (currentId - 1).coerceAtLeast(0)
|
||||
)
|
||||
}
|
||||
|
||||
private fun extractTime(timestamp: String): String {
|
||||
return try {
|
||||
timestamp.substringAfter("T").substring(0, 5)
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
stopPolling()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
package com.example.basurapp.models
|
||||
|
||||
/**
|
||||
* Dirección de un usuario. Colección "addresses" en Firestore.
|
||||
*/
|
||||
data class Address(
|
||||
val id: String = "",
|
||||
val userId: String = "",
|
||||
val street: String = "",
|
||||
val number: String = "",
|
||||
val neighborhood: String = "",
|
||||
val city: String = "",
|
||||
val neighborhood: String = "", // ← este es el nombre exacto de la colonia
|
||||
val city: String = "Celaya",
|
||||
val reference: String = ""
|
||||
)
|
||||
20
app/src/main/java/com/example/basurapp/models/ApiRoute.kt
Normal file
20
app/src/main/java/com/example/basurapp/models/ApiRoute.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.example.basurapp.models
|
||||
|
||||
/**
|
||||
* Modelo de la API de rutas (rutas.json)
|
||||
*/
|
||||
data class ApiRoute(
|
||||
val routeId: String = "",
|
||||
val name: String = "",
|
||||
val truckId: Int = 0,
|
||||
val status: String = "",
|
||||
val positions: List<ApiRoutePosition> = emptyList()
|
||||
)
|
||||
|
||||
data class ApiRoutePosition(
|
||||
val positionId: Int = 0,
|
||||
val lat: Double = 0.0,
|
||||
val lng: Double = 0.0,
|
||||
val speed: Int = 0,
|
||||
val timestamp: String = "" // formato ISO: "2026-05-22T06:00:00Z"
|
||||
)
|
||||
10
app/src/main/java/com/example/basurapp/models/ColoniaRuta.kt
Normal file
10
app/src/main/java/com/example/basurapp/models/ColoniaRuta.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package com.example.basurapp.models
|
||||
|
||||
/**
|
||||
* Modelo de colonias-rutas.json
|
||||
*/
|
||||
data class ColoniaRuta(
|
||||
val colonia: String = "",
|
||||
val routeId: String = "",
|
||||
val horarioEstimado: String = ""
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.basurapp.models
|
||||
|
||||
/**
|
||||
* Modelo de notificaciones.json
|
||||
*/
|
||||
data class NotificationRule(
|
||||
val triggerEvent: String = "",
|
||||
val condition: String = "",
|
||||
val pushPayload: PushPayload = PushPayload()
|
||||
) {
|
||||
companion object {
|
||||
const val EVENT_ROUTE_START = "ROUTE_START"
|
||||
const val EVENT_TRUCK_PROXIMITY = "TRUCK_PROXIMITY"
|
||||
const val EVENT_ROUTE_COMPLETED = "ROUTE_COMPLETED"
|
||||
}
|
||||
}
|
||||
|
||||
data class PushPayload(
|
||||
val title: String = "",
|
||||
val body: String = ""
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.example.basurapp.utils
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.example.basurapp.MainActivity
|
||||
import com.example.basurapp.R
|
||||
|
||||
object NotificationHelper {
|
||||
|
||||
private const val CHANNEL_ID = "basurapp_routes"
|
||||
private const val CHANNEL_NAME = "Rutas de recolección"
|
||||
|
||||
fun ensureChannel(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
description = "Notificaciones sobre el camión recolector"
|
||||
}
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
fun show(context: Context, notificationId: Int, title: String, body: String) {
|
||||
ensureChannel(context)
|
||||
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context, 0, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setContentTitle(title)
|
||||
.setContentText(body)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
|
||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
manager.notify(notificationId, builder.build())
|
||||
}
|
||||
}
|
||||
96
app/src/main/java/com/example/basurapp/utils/RouteTracker.kt
Normal file
96
app/src/main/java/com/example/basurapp/utils/RouteTracker.kt
Normal file
@@ -0,0 +1,96 @@
|
||||
package com.example.basurapp.utils
|
||||
|
||||
import android.util.Log
|
||||
import com.example.basurapp.models.ApiRoute
|
||||
import com.example.basurapp.models.ApiRoutePosition
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
object RouteTracker {
|
||||
|
||||
private const val TAG = "RouteTracker"
|
||||
|
||||
private val isoFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
|
||||
|
||||
// ======== MODO DEMO ========
|
||||
// Cambia DEMO_ENABLED a false para usar la hora real del dispositivo
|
||||
private const val DEMO_ENABLED = true
|
||||
private const val DEMO_START_HOUR = 6
|
||||
private const val DEMO_START_MINUTE = 10
|
||||
private const val DEMO_SPEED = 60 // cada segundo real = X minutos simulados
|
||||
|
||||
private var demoStartRealTimeMs: Long = 0L
|
||||
// ===========================
|
||||
|
||||
fun getCurrentPositionId(route: ApiRoute): Int {
|
||||
if (route.positions.isEmpty()) return 0
|
||||
|
||||
val nowMinutes = currentMinutesOfDay()
|
||||
var currentId = 0
|
||||
|
||||
for (pos in route.positions.sortedBy { it.positionId }) {
|
||||
val posMinutes = timestampToMinutesOfDay(pos.timestamp) ?: continue
|
||||
if (nowMinutes >= posMinutes) {
|
||||
currentId = pos.positionId
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "Ruta ${route.routeId}: hora simulada = ${nowMinutes / 60}:${nowMinutes % 60}, positionId = $currentId")
|
||||
return currentId
|
||||
}
|
||||
|
||||
fun getCurrentPosition(route: ApiRoute): ApiRoutePosition? {
|
||||
val currentId = getCurrentPositionId(route)
|
||||
if (currentId == 0) return null
|
||||
return route.positions.firstOrNull { it.positionId == currentId }
|
||||
}
|
||||
|
||||
fun getProgress(route: ApiRoute): Float {
|
||||
val currentId = getCurrentPositionId(route)
|
||||
val total = route.positions.size
|
||||
if (total == 0) return 0f
|
||||
return currentId.toFloat() / total.toFloat()
|
||||
}
|
||||
|
||||
fun getStatusText(route: ApiRoute): String {
|
||||
val currentId = getCurrentPositionId(route)
|
||||
val total = route.positions.size
|
||||
return when {
|
||||
currentId == 0 -> "Aún no inicia la ruta"
|
||||
currentId == total -> "Ruta completada"
|
||||
currentId == total - 1 -> "Regresando al depósito"
|
||||
else -> "En ruta (parada $currentId de $total)"
|
||||
}
|
||||
}
|
||||
|
||||
private fun currentMinutesOfDay(): Int {
|
||||
if (DEMO_ENABLED) {
|
||||
// Inicializar el reloj demo la primera vez
|
||||
if (demoStartRealTimeMs == 0L) {
|
||||
demoStartRealTimeMs = System.currentTimeMillis()
|
||||
Log.d(TAG, "Demo iniciado en ${DEMO_START_HOUR}:${DEMO_START_MINUTE}")
|
||||
}
|
||||
val elapsedSeconds = (System.currentTimeMillis() - demoStartRealTimeMs) / 1000
|
||||
val simulatedMinutesElapsed = elapsedSeconds * DEMO_SPEED / 60
|
||||
val startMinutes = DEMO_START_HOUR * 60 + DEMO_START_MINUTE
|
||||
return (startMinutes + simulatedMinutesElapsed).toInt()
|
||||
}
|
||||
|
||||
// Hora real del dispositivo
|
||||
val cal = java.util.Calendar.getInstance()
|
||||
return cal.get(java.util.Calendar.HOUR_OF_DAY) * 60 + cal.get(java.util.Calendar.MINUTE)
|
||||
}
|
||||
|
||||
private fun timestampToMinutesOfDay(timestamp: String): Int? {
|
||||
return try {
|
||||
val date: Date = isoFormat.parse(timestamp) ?: return null
|
||||
val cal = java.util.Calendar.getInstance().apply { time = date }
|
||||
cal.get(java.util.Calendar.HOUR_OF_DAY) * 60 + cal.get(java.util.Calendar.MINUTE)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
94
app/src/main/res/layout/activity_add_address.xml
Normal file
94
app/src/main/res/layout/activity_add_address.xml
Normal file
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/white"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center"
|
||||
android:text="Agregar dirección"
|
||||
android:textColor="@color/gray_900"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:hint="Calle">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etStreet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPostalAddress" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="Número">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etNumber"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="Colonia"
|
||||
app:endIconMode="dropdown_menu">
|
||||
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
android:id="@+id/actvColonia"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="Referencia (opcional)">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etReference"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSave"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="Guardar"
|
||||
app:cornerRadius="28dp" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
113
app/src/main/res/layout/fragment_home.xml
Normal file
113
app/src/main/res/layout/fragment_home.xml
Normal file
@@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/white">
|
||||
|
||||
<!-- Estado "sin dirección" -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutNoAddress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/no_address_title"
|
||||
android:textColor="@color/gray_900"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/no_routes"
|
||||
android:textColor="@color/gray_900"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnAddAddressEmpty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:text="@string/add_address"
|
||||
app:cornerRadius="24dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Estado con direcciones -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutWithAddresses"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<!-- Sección de mis direcciones -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Mis direcciones"
|
||||
android:textColor="@color/gray_900"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnAddMoreAddress"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="+ Agregar"
|
||||
android:textColor="@color/green_primary" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvAddresses"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
tools:itemCount="2"
|
||||
tools:listitem="@layout/item_address" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@color/gray_200" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Rutas activas"
|
||||
android:textColor="@color/gray_900"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvRoutes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_weight="1"
|
||||
android:clipToPadding="false"
|
||||
tools:listitem="@layout/item_route" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
61
app/src/main/res/layout/item_address.xml
Normal file
61
app/src/main/res/layout/item_address.xml
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="1dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvStreet"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/gray_900"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnDelete"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvNeighborhood"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="@color/green_primary"
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnDelete"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvStreet" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvReference"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="@color/gray_500"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnDelete"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvNeighborhood" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnDelete"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Eliminar"
|
||||
android:src="@android:drawable/ic_menu_delete"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -18,7 +18,7 @@
|
||||
android:id="@+id/tvRouteName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/gray_900"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user