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 {
|
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.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("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation("com.google.android.material:material:1.11.0")
|
implementation("com.google.android.material:material:1.11.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
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.fragment:fragment-ktx:1.6.2")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
|
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.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(platform("com.google.firebase:firebase-bom:32.7.4"))
|
||||||
implementation("com.google.firebase:firebase-auth-ktx")
|
implementation("com.google.firebase:firebase-auth-ktx")
|
||||||
implementation("com.google.firebase:firebase-firestore-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
|
// Coroutines
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
|
||||||
@@ -60,3 +71,13 @@ dependencies {
|
|||||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
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.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -15,6 +16,9 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.BasurApp"
|
android:theme="@style/Theme.BasurApp"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
<activity
|
||||||
|
android:name=".AddAddressActivity"
|
||||||
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".RegisterActivity"
|
android:name=".RegisterActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
@@ -30,6 +34,9 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".AddAddressActivity"
|
||||||
|
android:exported="false" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</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
|
// Si ya hay sesión activa, ir directo al Main
|
||||||
if (auth.currentUser != null) {
|
// if (auth.currentUser != null) {
|
||||||
goToMain()
|
// goToMain()
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
binding.tvCreateAccount.setOnClickListener {
|
binding.tvCreateAccount.setOnClickListener {
|
||||||
startActivity(Intent(this, RegisterActivity::class.java))
|
startActivity(Intent(this, RegisterActivity::class.java))
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package com.example.basurapp
|
package com.example.basurapp
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.example.basurapp.databinding.ActivityMainBinding
|
import com.example.basurapp.databinding.ActivityMainBinding
|
||||||
@@ -50,6 +54,20 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||||||
binding.navigationView.setNavigationItemSelectedListener(this)
|
binding.navigationView.setNavigationItemSelectedListener(this)
|
||||||
|
|
||||||
loadCurrentUser()
|
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() {
|
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
|
package com.example.basurapp.fragments
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
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.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.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.Route
|
||||||
import com.example.basurapp.models.RouteStop
|
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.auth.FirebaseAuth
|
||||||
import com.google.firebase.firestore.FirebaseFirestore
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class HomeFragment : Fragment() {
|
class HomeFragment : Fragment() {
|
||||||
|
|
||||||
private var _binding: HomeFragmentBinding? = null
|
private var _binding: FragmentHomeBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private val auth: FirebaseAuth by lazy { FirebaseAuth.getInstance() }
|
private val auth: FirebaseAuth by lazy { FirebaseAuth.getInstance() }
|
||||||
private val db: FirebaseFirestore by lazy { FirebaseFirestore.getInstance() }
|
private val db: FirebaseFirestore by lazy { FirebaseFirestore.getInstance() }
|
||||||
|
|
||||||
private lateinit var routesAdapter: RoutesAdapter
|
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(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
_binding = HomeFragmentBinding.inflate(inflater, container, false)
|
_binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,24 +75,50 @@ class HomeFragment : Fragment() {
|
|||||||
binding.rvRoutes.layoutManager = LinearLayoutManager(requireContext())
|
binding.rvRoutes.layoutManager = LinearLayoutManager(requireContext())
|
||||||
binding.rvRoutes.adapter = routesAdapter
|
binding.rvRoutes.adapter = routesAdapter
|
||||||
|
|
||||||
binding.btnAddAddress.setOnClickListener {
|
// Adapter de direcciones
|
||||||
Toast.makeText(requireContext(), "Función Agregar dirección (pendiente)", Toast.LENGTH_SHORT).show()
|
addressesAdapter = AddressesAdapter { address ->
|
||||||
// TODO: Abrir Activity o dialog para agregar dirección
|
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
|
val uid = auth.currentUser?.uid ?: return
|
||||||
db.collection("addresses")
|
db.collection("addresses")
|
||||||
.whereEqualTo("userId", uid)
|
.whereEqualTo("userId", uid)
|
||||||
.get()
|
.get()
|
||||||
.addOnSuccessListener { result ->
|
.addOnSuccessListener { result ->
|
||||||
if (result.isEmpty) {
|
userAddresses = result.documents.mapNotNull { it.toObject(Address::class.java) }
|
||||||
|
if (userAddresses.isEmpty()) {
|
||||||
showNoAddressState()
|
showNoAddressState()
|
||||||
|
stopPolling()
|
||||||
} else {
|
} else {
|
||||||
showRoutesState()
|
showWithAddressesState()
|
||||||
|
addressesAdapter.submitList(userAddresses)
|
||||||
|
startPolling()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.addOnFailureListener {
|
.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() {
|
private fun showNoAddressState() {
|
||||||
|
if (_binding == null) return
|
||||||
binding.layoutNoAddress.visibility = View.VISIBLE
|
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.layoutNoAddress.visibility = View.GONE
|
||||||
binding.rvRoutes.visibility = View.VISIBLE
|
binding.layoutWithAddresses.visibility = View.VISIBLE
|
||||||
loadRoutes()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadRoutes() {
|
private fun startPolling() {
|
||||||
// Por ahora datos de ejemplo (después se conectan a Firestore)
|
stopPolling()
|
||||||
val sampleRoutes = listOf(
|
handler.post(pollRunnable)
|
||||||
Route(
|
}
|
||||||
id = "1",
|
|
||||||
name = "Dirección 1",
|
private fun stopPolling() {
|
||||||
neighborhood = "Centro",
|
handler.removeCallbacks(pollRunnable)
|
||||||
stops = listOf(
|
}
|
||||||
RouteStop("Inicio", "07:00"),
|
|
||||||
RouteStop("Calle 5 de Mayo", "07:30"),
|
private fun loadNotificationRules() {
|
||||||
RouteStop("Plaza Central", "08:00"),
|
lifecycleScope.launch {
|
||||||
RouteStop("Av. Juárez", "08:30"),
|
try {
|
||||||
RouteStop("Final de ruta", "09:00")
|
notificationRules = ApiClient.api.getNotificationRules()
|
||||||
),
|
} catch (e: Exception) {
|
||||||
truckPosition = 2
|
// silenciar
|
||||||
),
|
}
|
||||||
Route(
|
}
|
||||||
id = "2",
|
}
|
||||||
name = "Dirección 1",
|
|
||||||
neighborhood = "Zona Norte",
|
private fun refreshRoutes() {
|
||||||
stops = listOf(
|
if (userAddresses.isEmpty()) return
|
||||||
RouteStop("Inicio", "10:00"),
|
|
||||||
RouteStop("Mercado Norte", "10:30"),
|
lifecycleScope.launch {
|
||||||
RouteStop("Parque Hidalgo", "11:00"),
|
try {
|
||||||
RouteStop("Final de ruta", "11:30")
|
if (coloniasRutas.isEmpty()) {
|
||||||
),
|
coloniasRutas = ApiClient.api.getColoniasRutas()
|
||||||
truckPosition = 1
|
}
|
||||||
)
|
|
||||||
|
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() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
stopPolling()
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
package com.example.basurapp.models
|
package com.example.basurapp.models
|
||||||
|
|
||||||
/**
|
|
||||||
* Dirección de un usuario. Colección "addresses" en Firestore.
|
|
||||||
*/
|
|
||||||
data class Address(
|
data class Address(
|
||||||
val id: String = "",
|
val id: String = "",
|
||||||
val userId: String = "",
|
val userId: String = "",
|
||||||
val street: String = "",
|
val street: String = "",
|
||||||
val number: String = "",
|
val number: String = "",
|
||||||
val neighborhood: String = "",
|
val neighborhood: String = "", // ← este es el nombre exacto de la colonia
|
||||||
val city: String = "",
|
val city: String = "Celaya",
|
||||||
val reference: String = ""
|
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:id="@+id/tvRouteName"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@color/gray_900"
|
android:textColor="?attr/colorOnSurface"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user