diff --git a/README.md b/README.md index b659d62..1f55212 100644 --- a/README.md +++ b/README.md @@ -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> +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> = 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. diff --git a/app/src/main/java/com/manalejandro/location/LocationService.kt b/app/src/main/java/com/manalejandro/location/LocationService.kt index b022db9..aaca035 100644 --- a/app/src/main/java/com/manalejandro/location/LocationService.kt +++ b/app/src/main/java/com/manalejandro/location/LocationService.kt @@ -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 + } + } + } } diff --git a/app/src/main/java/com/manalejandro/location/MainActivity.kt b/app/src/main/java/com/manalejandro/location/MainActivity.kt index 2ef45aa..3ffdc06 100644 --- a/app/src/main/java/com/manalejandro/location/MainActivity.kt +++ b/app/src/main/java/com/manalejandro/location/MainActivity.kt @@ -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?>(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( ) } - Button( - onClick = { - val lat = latitude.toDoubleOrNull() - val lng = longitude.toDoubleOrNull() - if (lat != null && lng != null) { - onMockLocation(lat, lng) + // 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") + } + } } - }, - enabled = latitude.isNotBlank() && longitude.isNotBlank() && !uiState.isLoading, - modifier = Modifier.fillMaxWidth() - ) { - Text("Establecer Ubicación Personalizada") + } + } + + // 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 && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) { + pendingLocation = Pair(lat, lng) + } + }, + enabled = latitude.isNotBlank() && longitude.isNotBlank() && !uiState.isLoading, + modifier = Modifier.fillMaxWidth() + ) { + Text("Preparar Ubicación Personalizada") + } } uiState.mockedLocation?.let { location -> LocationDisplay( - title = "Ubicación Guardada", + title = "Ubicación Establecida en Todos los Proveedores", location = location ) }