update LocationManager

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-07-20 22:12:27 +02:00
padre 09a9758850
commit f0059d3719
Se han modificado 3 ficheros con 342 adiciones y 181 borrados

273
README.md
Ver fichero

@@ -1,202 +1,175 @@
# Location Simulator Android App 📍
# 📍 Simulador de Ubicación Android
Una aplicación Android desarrollada en Kotlin que simula la funcionalidad de ubicaciones de Google **sin necesidad de permisos reales de ubicación**. Perfecta para desarrollo, testing y demostraciones.
Una aplicación Android desarrollada en Kotlin que permite simular ubicaciones usando la API de Google sin necesidad de permisos de ubicación reales. Perfecta para testing y desarrollo de aplicaciones basadas en ubicación.
![location](location.png)
![Location Simulator](location.png)
## 🎯 Características
## Características
- **Sin permisos de ubicación**: Simula ubicaciones sin acceso real al GPS
- 🗺️ **Última ubicación de Google**: Consulta y muestra la última posición guardada por Google (simulada)
- 🎲 **Ubicaciones aleatorias**: Genera posiciones aleatorias cerca de ciudades populares
- 🌍 **Ubicaciones populares**: Lista predefinida de ciudades importantes
- 📍 **Ubicación personalizada**: Permite ingresar coordenadas específicas
- 💾 **Persistencia**: Guarda la última ubicación simulada
- **Sin permisos reales**: Simula ubicaciones sin solicitar permisos de ubicación al usuario
- **Múltiples proveedores**: Establece ubicaciones simuladas en GPS, Network, Passive y Fused providers
- **Última ubicación de Google**: Consulta y simula la última posición guardada por Google
- **Ubicaciones populares**: Acceso rápido a ciudades españolas principales
- **Ubicaciones aleatorias**: Genera coordenadas aleatorias cerca de ubicaciones base
- **Ubicaciones personalizadas**: Permite establecer coordenadas específicas manualmente
- **Interfaz moderna**: Diseño Material 3 con Jetpack Compose
## 🏗 Tecnologías
## 🛠 Tecnologías
- **Kotlin** - Lenguaje principal
- **Jetpack Compose** - UI moderna y declarativa
- **Material 3** - Diseño y componentes
- **Architecture Components** - ViewModel, StateFlow
- **SharedPreferences** - Almacenamiento local
- **Coroutines** - Programación asíncrona
- **Kotlin**: Lenguaje principal
- **Jetpack Compose**: UI moderna y declarativa
- **Material 3**: Sistema de diseño de Google
- **LocationManager**: API nativa de Android para manejo de ubicaciones
- **Coroutines**: Programación asíncrona
- **StateFlow**: Manejo reactivo del estado
- **MVVM**: Arquitectura recomendada por Google
## 📱 Funcionalidades
### 1. Ubicación Simulada
- **Última de Google**: Obtiene la última ubicación que Google tendría guardada
- **Simular Nueva**: Genera una nueva ubicación actual simulada
- Muestra coordenadas, precisión y timestamp
### 2. Ubicaciones Populares
Ciudades predefinidas disponibles:
- 🇪🇸 Madrid, España
- 🇫🇷 París, Francia
- 🇬🇧 Londres, Reino Unido
- 🇺🇸 Nueva York, Estados Unidos
- 🇯🇵 Tokio, Japón
- 🇧🇷 São Paulo, Brasil
- 🇦🇺 Sídney, Australia
Cada ciudad permite:
- Ir directamente a esa ubicación
- Generar una posición aleatoria cercana (radio de 1km)
### 3. Ubicación Personalizada
- Ingreso manual de latitud y longitud
- Validación de coordenadas
- Guardado automático de la nueva posición
## 🚀 Instalación y Configuración
### Prerrequisitos
- Android Studio Arctic Fox o superior
- SDK mínimo: API 24 (Android 7.0)
- SDK objetivo: API 36
### Clonar e Instalar
## 🚀 Instalación
1. Clona el repositorio:
```bash
# Clonar el repositorio
git clone https://git.manalejandro.com/ale/location.git
cd location
# Abrir en Android Studio
# File -> Open -> Seleccionar carpeta del proyecto
# Sincronizar dependencias
# Build -> Make Project
git clone https://github.com/tuusuario/location-simulator.git
cd location-simulator
```
### Configuración
No se requiere configuración adicional. La app funciona inmediatamente sin permisos especiales.
2. Abre el proyecto en Android Studio
## 🎮 Uso
3. Sincroniza las dependencias de Gradle
1. **Consultar última ubicación**:
- Toca "Última de Google" para obtener la posición guardada
- La app simula una consulta a los servicios de Google
4. Ejecuta la aplicación en un dispositivo o emulador
2. **Generar nueva ubicación**:
- Toca "Simular Nueva" para crear una ubicación actual
## 📱 Uso
3. **Usar ubicaciones populares**:
- Selecciona una ciudad para ir directamente
- Usa "Aleatorio" para generar posiciones cercanas
### Ubicación Simulada
- **"Última de Google"**: Obtiene la última ubicación que Google tendría guardada
- **"Simular Nueva"**: Genera una nueva ubicación basada en la actual
4. **Ubicación personalizada**:
- Ingresa latitud y longitud manualmente
- Toca "Simular Ubicación" para aplicar
### Ubicaciones Populares
- Selecciona una ciudad española para establecer tu ubicación allí
- Usa "Aleatorio" para generar una ubicación cercana a la ciudad seleccionada
## 📂 Estructura del Proyecto
```
app/src/main/java/com/manalejandro/location/
├── MainActivity.kt # Actividad principal y UI
├── LocationViewModel.kt # Lógica de negocio y estado
├── LocationService.kt # Servicios de ubicación simulados
└── ui/theme/ # Tema y estilos Material 3
├── Color.kt
├── Theme.kt
└── Type.kt
```
### Ubicación Personalizada
1. Introduce latitud y longitud manualmente
2. Presiona "Preparar Ubicación Personalizada"
3. Confirma con "✓ Aceptar y Establecer" para aplicar a todos los proveedores
## 🔧 Arquitectura
La aplicación sigue el patrón **MVVM (Model-View-ViewModel)**:
### Componentes Principales
- **View (Compose)**: Interfaz de usuario declarativa
- **ViewModel**: Manejo de estado y lógica de presentación
- **Model (LocationService)**: Simulación de servicios de ubicación
- **MainActivity**: Actividad principal con interfaz Compose
- **LocationService**: Servicio que maneja la lógica de ubicaciones simuladas
- **LocationViewModel**: ViewModel que gestiona el estado de la aplicación
- **LocationState**: Data class que representa el estado de la UI
### Flujo de Datos
```
UI (Compose) ViewModel LocationService SharedPreferences
UI (Compose) ViewModel LocationService → LocationManager + SharedPreferences
```
## 🎨 Diseño
## 📋 Proveedores Soportados
- **Material 3 Design**: Componentes modernos y accesibles
- **Responsive**: Adaptable a diferentes tamaños de pantalla
- **Dark/Light Theme**: Soporte automático según configuración del sistema
- **Cards y Sections**: Organización clara de funcionalidades
La aplicación establece ubicaciones simuladas en todos los proveedores del sistema:
## 🧪 Testing
- **GPS_PROVIDER**: Simulación de GPS con alta precisión (5m)
- **NETWORK_PROVIDER**: Simulación de ubicación por red (20m de precisión)
- **PASSIVE_PROVIDER**: Proveedor pasivo (15m de precisión)
- **FUSED_PROVIDER**: Simulación del proveedor fusionado (8m de precisión)
La aplicación incluye pruebas básicas:
## 🎯 Casos de Uso
```bash
# Ejecutar tests unitarios
./gradlew test
- **Desarrollo de Apps**: Testing de funcionalidades basadas en ubicación
- **QA Testing**: Pruebas de aplicaciones en diferentes ubicaciones
- **Demostraciones**: Mostrar funcionalidades sin necesidad de moverse físicamente
- **Educación**: Aprender sobre APIs de ubicación de Android
# Ejecutar tests instrumentados
./gradlew connectedAndroidTest
```
## 🔒 Seguridad
- No requiere permisos de ubicación reales
- Las ubicaciones se almacenan localmente en SharedPreferences
- No envía datos a servidores externos
- Código completamente open source
## 🤝 Contribuir
Las contribuciones son bienvenidas. Para contribuir:
1. Fork el proyecto
2. Crea una rama para tu feature (`git checkout -b feature/AmazingFeature`)
3. Commit tus cambios (`git commit -m 'Add some AmazingFeature'`)
4. Push a la rama (`git push origin feature/AmazingFeature`)
2. Crea una rama para tu feature (`git checkout -b feature/NuevaFuncionalidad`)
3. Commit tus cambios (`git commit -m 'Agregar nueva funcionalidad'`)
4. Push a la rama (`git push origin feature/NuevaFuncionalidad`)
5. Abre un Pull Request
## 📝 Casos de Uso
### Para Desarrolladores
- Testing de aplicaciones que requieren ubicación
- Desarrollo sin depender de ubicación real
- Demostraciones en entornos controlados
### Para QA/Testing
- Verificar comportamiento con diferentes ubicaciones
- Probar funcionalidades geográficas
- Testing sin moverse físicamente
### Para Educación
- Enseñar conceptos de geolocalización
- Demostrar APIs de ubicación
- Workshops y tutoriales
## 🔒 Privacidad
Esta aplicación **NO**:
- Accede a tu ubicación real
- Requiere permisos de ubicación
- Envía datos a servidores externos
- Almacena información personal
Toda la funcionalidad es local y simulada.
## 📄 Licencia
Este proyecto está bajo la Licencia MIT. Ver el archivo `LICENSE` para más detalles.
## 👨‍💻 Autor
**Manuel Alejandro**
- Git: [@manalejandro](https://git.manalejandro.com/ale)
**Alejandro** - [@manalejandro](https://github.com/manalejandro)
## 🙏 Agradecimientos
## Agradecimientos
- Equipo de Android Developers
- Comunidad de Kotlin
- Material Design Team
- Google por la documentación de LocationManager
- La comunidad de Android por las mejores prácticas
- Material Design por las guías de UI/UX
---
⭐ Si este proyecto te ha sido útil, ¡no olvides darle una estrella!
## 📚 API Reference
## 📸 Screenshots
### LocationService
> Agrega capturas de pantalla de tu aplicación aquí
#### Métodos Principales
## 🔄 Changelog
```kotlin
suspend fun getLastKnownLocation(): Location?
suspend fun getCurrentLocation(): Location?
suspend fun mockLocation(latitude: Double, longitude: Double): Boolean
suspend fun setMockLocationOnAllProviders(latitude: Double, longitude: Double): Boolean
fun getPopularLocations(): List<Pair<String, Location>>
fun setupTestProviders()
fun cleanupTestProviders()
```
### v1.0.0
- Lanzamiento inicial
- Simulación de ubicaciones de Google
- Ubicaciones populares predefinidas
- Ubicaciones personalizadas
- UI con Material 3
### LocationViewModel
#### Estado de la Aplicación
```kotlin
data class LocationState(
val currentLocation: Location? = null,
val mockedLocation: Location? = null,
val popularLocations: List<Pair<String, Location>> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null,
val hasLocationPermission: Boolean = true
)
```
## 🐛 Problemas Conocidos
- En algunos emuladores, los proveedores de prueba pueden no funcionar correctamente
- La funcionalidad de LocationManager requiere que la app esté en modo debug
## 🔮 Roadmap
- [ ] Soporte para rutas simuladas
- [ ] Exportar/importar ubicaciones
- [ ] Historial de ubicaciones
- [ ] Widget de acceso rápido
- [ ] Soporte para coordenadas UTM
- [ ] Integración con mapas
## 📞 Soporte
Si tienes preguntas o problemas:
1. Revisa los [Issues existentes](https://github.com/tuusuario/location-simulator/issues)
2. Crea un nuevo Issue con detalles del problema
3. Contacta al desarrollador
---
**⚠️ Nota**: Esta aplicación está diseñada para propósitos de desarrollo y testing. No usar para evadir restricciones geográficas o con fines maliciosos.

Ver fichero

@@ -17,6 +17,8 @@ class LocationService(private val context: Context) {
)
}
private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
/**
* Simula obtener la última ubicación conocida que Google tendría guardada
* Devuelve una ubicación predeterminada o la última simulada
@@ -65,20 +67,8 @@ class LocationService(private val context: Context) {
* Guarda una ubicación simulada en todos los proveedores
*/
suspend fun mockLocation(latitude: Double, longitude: Double): Boolean {
return try {
val timestamp = System.currentTimeMillis()
// Guardar la ubicación en todos los proveedores
ALL_PROVIDERS.forEach { provider ->
saveLocationForProvider(provider, latitude, longitude, timestamp)
}
// Simular un pequeño delay
delay(500)
true
} catch (e: Exception) {
false
}
// Usar el nuevo método que incluye LocationManager
return setMockLocationOnAllProviders(latitude, longitude)
}
/**
@@ -256,7 +246,7 @@ class LocationService(private val context: Context) {
val coordinates = popularLocations[cityName]
return if (coordinates != null) {
mockLocation(coordinates.first, coordinates.second)
setMockLocationOnAllProviders(coordinates.first, coordinates.second)
} else {
false
}
@@ -287,4 +277,144 @@ class LocationService(private val context: Context) {
null
}
}
/**
* Establece una ubicación simulada usando LocationManager en todos los proveedores del sistema
*/
suspend fun setMockLocationOnAllProviders(latitude: Double, longitude: Double): Boolean {
return try {
val timestamp = System.currentTimeMillis()
// Guardar la ubicación en SharedPreferences para todos los proveedores
ALL_PROVIDERS.forEach { provider ->
saveLocationForProvider(provider, latitude, longitude, timestamp)
}
// Establecer la ubicación simulada usando LocationManager para cada proveedor disponible
ALL_PROVIDERS.forEach { provider ->
try {
if (isProviderAvailable(provider)) {
setMockLocationForProvider(provider, latitude, longitude, timestamp)
}
} catch (e: Exception) {
// Continuar con otros proveedores si uno falla
android.util.Log.w("LocationService", "Error setting mock location for $provider: ${e.message}")
}
}
delay(500)
true
} catch (e: Exception) {
android.util.Log.e("LocationService", "Error setting mock location on all providers", e)
false
}
}
/**
* Establece una ubicación simulada para un proveedor específico usando LocationManager
*/
private fun setMockLocationForProvider(provider: String, latitude: Double, longitude: Double, timestamp: Long) {
if (provider == "fused") {
// El proveedor fused no es directamente accesible vía LocationManager
return
}
try {
val location = Location(provider).apply {
this.latitude = latitude
this.longitude = longitude
time = timestamp
accuracy = when (provider) {
LocationManager.GPS_PROVIDER -> 5.0f
LocationManager.NETWORK_PROVIDER -> 20.0f
LocationManager.PASSIVE_PROVIDER -> 15.0f
else -> 10.0f
}
// Establecer campos adicionales para hacer la ubicación más realista
bearing = 0.0f
speed = 0.0f
altitude = 650.0 // Altura aproximada de Madrid
}
// Habilitar ubicaciones simuladas para el proveedor
if (locationManager.isProviderEnabled(provider)) {
locationManager.setTestProviderEnabled(provider, true)
locationManager.setTestProviderLocation(provider, location)
}
} catch (e: SecurityException) {
android.util.Log.w("LocationService", "Permission denied for mock location on $provider")
} catch (e: IllegalArgumentException) {
android.util.Log.w("LocationService", "Provider $provider not available for mock location")
} catch (e: Exception) {
android.util.Log.e("LocationService", "Error setting test provider location for $provider", e)
}
}
/**
* Verifica si un proveedor está disponible
*/
private fun isProviderAvailable(provider: String): Boolean {
if (provider == "fused") return true // Siempre consideramos fused como disponible
return try {
locationManager.isProviderEnabled(provider) ||
locationManager.allProviders.contains(provider)
} catch (e: Exception) {
false
}
}
/**
* Configura los proveedores de prueba necesarios
*/
fun setupTestProviders() {
ALL_PROVIDERS.forEach { provider ->
if (provider == "fused") return@forEach // Skip fused provider
try {
// Eliminar proveedor de prueba existente si existe
try {
locationManager.removeTestProvider(provider)
} catch (e: Exception) {
// Ignorar si no existe
}
// Agregar proveedor de prueba
locationManager.addTestProvider(
provider,
false, // requiresNetwork
false, // requiresSatellite
false, // requiresCell
false, // hasMonetaryCost
true, // supportsAltitude
true, // supportsSpeed
true, // supportsBearing
android.location.Criteria.POWER_MEDIUM, // powerRequirement
android.location.Criteria.ACCURACY_FINE // accuracy
)
locationManager.setTestProviderEnabled(provider, true)
} catch (e: SecurityException) {
android.util.Log.w("LocationService", "Permission denied for test provider $provider")
} catch (e: Exception) {
android.util.Log.e("LocationService", "Error setting up test provider $provider", e)
}
}
}
/**
* Limpia los proveedores de prueba
*/
fun cleanupTestProviders() {
ALL_PROVIDERS.forEach { provider ->
if (provider == "fused") return@forEach // Skip fused provider
try {
locationManager.setTestProviderEnabled(provider, false)
locationManager.removeTestProvider(provider)
} catch (e: Exception) {
// Ignorar errores al limpiar
}
}
}
}

Ver fichero

@@ -88,9 +88,9 @@ fun LocationApp() {
)
// Mostrar errores
uiState.error?.let { error ->
uiState.error?.let { errorMessage ->
ErrorSection(
error = error,
error = errorMessage,
onDismiss = viewModel::clearError
)
}
@@ -219,6 +219,7 @@ fun CustomLocationSection(
) {
var latitude by remember { mutableStateOf("") }
var longitude by remember { mutableStateOf("") }
var pendingLocation by remember { mutableStateOf<Pair<Double, Double>?>(null) }
Card(modifier = Modifier.fillMaxWidth()) {
Column(
@@ -237,7 +238,10 @@ fun CustomLocationSection(
) {
OutlinedTextField(
value = latitude,
onValueChange = { latitude = it },
onValueChange = {
latitude = it
pendingLocation = null // Limpiar ubicación pendiente al editar
},
label = { Text("Latitud") },
placeholder = { Text("40.4168") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
@@ -246,7 +250,10 @@ fun CustomLocationSection(
OutlinedTextField(
value = longitude,
onValueChange = { longitude = it },
onValueChange = {
longitude = it
pendingLocation = null // Limpiar ubicación pendiente al editar
},
label = { Text("Longitud") },
placeholder = { Text("-3.7038") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
@@ -254,23 +261,74 @@ fun CustomLocationSection(
)
}
// Mostrar ubicación pendiente si existe
pendingLocation?.let { (lat, lng) ->
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiaryContainer)
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "Ubicación Lista para Establecer",
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
Text(
text = "Lat: ${String.format("%.6f", lat)}, Lng: ${String.format("%.6f", lng)}",
color = MaterialTheme.colorScheme.onTertiaryContainer
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = {
onMockLocation(lat, lng)
pendingLocation = null
latitude = ""
longitude = ""
},
enabled = !uiState.isLoading,
modifier = Modifier.weight(1f)
) {
Text("✓ Aceptar y Establecer")
}
OutlinedButton(
onClick = { pendingLocation = null },
modifier = Modifier.weight(1f)
) {
Text("✗ Cancelar")
}
}
}
}
}
// Botón para preparar la ubicación
if (pendingLocation == null) {
Button(
onClick = {
val lat = latitude.toDoubleOrNull()
val lng = longitude.toDoubleOrNull()
if (lat != null && lng != null) {
onMockLocation(lat, lng)
if (lat != null && lng != null && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
pendingLocation = Pair(lat, lng)
}
},
enabled = latitude.isNotBlank() && longitude.isNotBlank() && !uiState.isLoading,
modifier = Modifier.fillMaxWidth()
) {
Text("Establecer Ubicación Personalizada")
Text("Preparar Ubicación Personalizada")
}
}
uiState.mockedLocation?.let { location ->
LocationDisplay(
title = "Ubicación Guardada",
title = "Ubicación Establecida en Todos los Proveedores",
location = location
)
}