Actualizaciones de ubicación: prioridad, primer plano y segundo plano
Cómo se construye un LocationRequest con la prioridad correcta, qué significan de verdad el intervalo mínimo y el retardo máximo, por qué el sistema estrangula las actualizaciones en segundo plano, y qué exige Android moderno para seguir recibiendo posición con la pantalla apagada: servicio en primer plano tipificado, permiso de fondo en dos pasos y una justificación que el usuario acepte.
Pedir una posición es una transacción; pedir un flujo de posiciones es un contrato de largo plazo con el sistema operativo, y ese contrato tiene cláusulas que el desarrollador no negocia. El sistema decide cuándo despertar la radio, cuándo agrupar entregas, cuándo estrangular la cadencia hasta unas pocas veces por hora y cuándo cortar del todo porque la aplicación no tiene derecho a seguir mirando. Escribir bien un LocationRequest consiste en expresar la necesidad real con la máxima honestidad posible, para que el planificador del sistema pueda ser generoso donde importa y tacaño donde no.
- Construir un
LocationRequesteligiendo la prioridad, el intervalo y el retardo máximo con criterio. - Distinguir el intervalo deseado del intervalo mínimo y del agrupamiento diferido.
- Explicar el estrangulamiento de actualizaciones en segundo plano y cómo lo evita un servicio en primer plano tipificado.
- Solicitar
ACCESS_BACKGROUND_LOCATIONcon el flujo en dos pasos que exige el sistema.
El contrato: prioridad, intervalos y agrupamiento
Un LocationRequest no ordena nada, declara. Cada parámetro es una pista que el planificador de ubicación usa para amortizar trabajo entre todos los clientes del dispositivo, y por eso el sistema puede entregar más rápido de lo pedido si otra aplicación ya estaba pagando la fijación, o más lento si el entorno no colabora.
La prioridad selecciona qué fuentes se permite encender. PRIORITY_HIGH_ACCURACY autoriza el GNSS y es la única adecuada para navegación o registro de rutas. PRIORITY_BALANCED_POWER_ACCURACY se queda en el orden de la decena de metros apoyándose en redes y en fijaciones oportunistas, y es el valor por defecto correcto para casi todo lo demás. PRIORITY_LOW_POWER opera a escala de manzana o barrio, y PRIORITY_PASSIVE no enciende absolutamente nada: se limita a escuchar las posiciones que otras aplicaciones ya provocaron, lo que la convierte en la opción de coste marginal cero para funcionalidades oportunistas.
Los tiempos son tres y se confunden con facilidad. El intervalo principal es la cadencia deseada. setMinUpdateIntervalMillis es el suelo: la entrega más rápida que la aplicación es capaz de procesar, y protege de la ráfaga que llega cuando otro cliente está pidiendo alta frecuencia. setMaxUpdateDelayMillis es la palanca energética más rentable de toda la API, porque autoriza al sistema a acumular varias posiciones y entregarlas juntas en un único despertar del procesador.
val peticion = LocationRequest.Builder(
Priority.PRIORITY_BALANCED_POWER_ACCURACY,
TimeUnit.SECONDS.toMillis(30),
)
.setMinUpdateIntervalMillis(TimeUnit.SECONDS.toMillis(10))
.setMinUpdateDistanceMeters(25f)
.setMaxUpdateDelayMillis(TimeUnit.MINUTES.toMillis(2))
.setWaitForAccurateLocation(false)
.build()
El filtro por distancia mínima merece atención aparte: suprime entregas mientras el usuario permanece quieto, que es exactamente el caso en el que las actualizaciones periódicas son puro desperdicio. Y setWaitForAccurateLocation en falso indica que se prefiere una primera posición mediocre pronto a una excelente tarde, lo cual casi siempre es lo que la interfaz necesita para dejar de mostrar un indicador de carga.
Si la funcionalidad tolera latencia —un registro de recorrido, una sincronización periódica, un histórico— fijar setMaxUpdateDelayMillis a varios minutos permite al sistema agrupar entregas y alinearlas con despertares que ya iban a ocurrir por otras razones. El ahorro procede de que el coste dominante no es medir, sino sacar al procesador del reposo profundo: diez entregas agrupadas en un despertar cuestan una fracción de diez despertares. Es el único parámetro que mejora la batería sin degradar ni la precisión ni la cadencia efectiva de los datos recibidos.
flowchart TD
A[LocationRequest] --> B{app visible}
B -- si --> C[cadencia solicitada]
B -- no --> D{servicio en primer plano tipo location}
D -- si --> C
D -- no --> E[estrangulado a unas pocas por hora]
C --> F[callback o PendingIntent]
E --> F
style C fill:#a6e3a1,color:#11111b
style E fill:#f38ba8,color:#11111bPrimer plano: ciclo de vida y entrega
En primer plano la regla es simple y estricta: las actualizaciones se registran cuando la interfaz es visible y se retiran cuando deja de serlo. Una suscripción huérfana es una fuga de batería que sobrevive a la pantalla y que ninguna herramienta señala como error.
private val callback = object : LocationCallback() {
override fun onLocationResult(resultado: LocationResult) {
resultado.locations
.filter { it.hasAccuracy() && it.accuracy < 50f }
.forEach { procesar(it) }
}
override fun onLocationAvailability(disponibilidad: LocationAvailability) {
if (!disponibilidad.isLocationAvailable) mostrarSinSenal()
}
}
override fun onStart() {
super.onStart()
cliente.requestLocationUpdates(peticion, callback, Looper.getMainLooper())
}
override fun onStop() {
super.onStop()
cliente.removeLocationUpdates(callback)
}
Dos detalles del fragmento son estructurales. El primero es que onLocationResult recibe una lista, no una posición: cuando el sistema agrupa entregas llegan varias de golpe, y el código que solo lee la última descarta información ya pagada. El segundo es onLocationAvailability, la única señal fiable de que el dispositivo ha perdido la capacidad de fijar posición; sin ella la interfaz se queda congelada en el último punto conocido sin poder explicar por qué.
En capas modernas conviene envolver la suscripción en un flujo frío que se registre al primer coleccionista y se desregistre al último, de modo que el ciclo de vida deje de ser responsabilidad de cada pantalla.
Segundo plano: estrangulamiento, servicio tipificado y permiso en dos pasos
Cuando la aplicación deja de ser visible, el sistema aplica un límite duro a las actualizaciones de las aplicaciones en segundo plano, del orden de unas pocas entregas por hora, con independencia de lo que pida el LocationRequest. Es una decisión deliberada de plataforma, no un fallo, y no se puede eludir con trucos.
Solo hay dos vías legítimas para recibir posición continua sin interfaz visible. La primera es un servicio en primer plano con tipo location declarado, lo que implica una notificación permanente y visible, el permiso FOREGROUND_SERVICE_LOCATION en el manifiesto y, desde Android 14, que el servicio se inicie desde un contexto válido. La segunda es no pedir un flujo, sino suscribirse a eventos: geofences o transiciones de actividad, que el sistema evalúa con hardware de bajo consumo y entrega solo cuando ocurre algo relevante.
En el manifiesto eso se traduce en declarar el permiso FOREGROUND_SERVICE_LOCATION y marcar el servicio con el atributo foregroundServiceType con valor location. En el código, el servicio debe promoverse a primer plano de inmediato y con el tipo correcto, o el sistema lo aborta:
class SeguimientoService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
ServiceCompat.startForeground(
this,
ID_NOTIFICACION,
construirNotificacion(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION,
)
cliente.requestLocationUpdates(peticion, callback, Looper.getMainLooper())
return START_STICKY
}
override fun onDestroy() {
cliente.removeLocationUpdates(callback)
super.onDestroy()
}
}
El permiso ACCESS_BACKGROUND_LOCATION es aparte y tiene su propio protocolo. Desde Android 11 no puede solicitarse en el mismo diálogo que los permisos de primer plano: hay que obtener antes el permiso en uso, explicar después con la interfaz de la aplicación por qué se necesita el acceso permanente, y solo entonces lanzar la petición, que abre los ajustes del sistema donde el usuario debe elegir explícitamente la opción de permitir siempre.
Primero, en uso
Solicitar ACCESS_FINE_LOCATION junto a ACCESS_COARSE_LOCATION y demostrar valor con la aplicación abierta antes de pedir más.
Después, justificar
Una pantalla propia que explique qué funcionalidad concreta deja de existir sin acceso permanente, con un ejemplo verificable.
Por último, el sistema
ACCESS_BACKGROUND_LOCATION en solitario. El sistema abre sus ajustes: la aplicación no controla ni el texto ni el resultado.
Y prever el no
La negativa es el caso frecuente. El producto debe seguir siendo útil con acceso solo en uso, o no merecía pedirlo.
Un permiso de ubicación en segundo plano concedido no es un permiso conservado. El sistema revoca automáticamente los permisos de las aplicaciones que el usuario no abre durante meses, muestra recordatorios periódicos que nombran a la aplicación y le ofrecen retirar el acceso, y expone en los ajustes de privacidad un registro de accesos recientes. A eso se suma la revisión de las tiendas, que exige una justificación funcional explícita para el acceso en segundo plano y rechaza los usos accesorios. La conclusión práctica es que el acceso permanente hay que merecerlo de forma continuada: cada acceso debe corresponder a un beneficio que el usuario reconozca al verlo en el registro.
La tentación de todo desarrollador ante una API de actualizaciones es pedir el máximo por si acaso: alta precisión, un segundo de intervalo, sin retardo y en segundo plano, con la idea de filtrar después lo que sobre. Es exactamente la estrategia inversa a la correcta, y su coste no lo paga la aplicación sino el usuario, que ve la batería caer sin poder atribuir la causa. El razonamiento experto invierte el orden. Se parte de la pregunta funcional —qué decisión toma el producto con cada posición— y se deriva de ahí la cadencia mínima que sostiene esa decisión, no al revés. Un registro de rutas necesita continuidad, pero tolera minutos de retardo en la entrega. Un aviso de proximidad no necesita ninguna actualización periódica: necesita un geofence. Un contenido regional no necesita metros: necesita una lectura aproximada al abrir. Cuando el requisito se expresa así, casi siempre resulta que el flujo continuo de alta precisión era una comodidad de implementación disfrazada de necesidad de producto. Y hay una asimetría que conviene interiorizar: pedir de más nunca produce un error visible en desarrollo, solo un consumo que aparece semanas después en las reseñas y en las desinstalaciones. Es una deuda silenciosa. El desarrollador que la evita no lo hace por altruismo, sino porque el sistema operativo premia al frugal con permisos que sobreviven y castiga al glotón con revocaciones, estrangulamientos y usuarios que aprenden a negar el diálogo por defecto.
- Registra una ruta durante treinta minutos con alta precisión sin retardo, repite con retardo máximo de dos minutos y compara consumo y número de puntos útiles.
- Deja la aplicación en segundo plano sin servicio en primer plano y cuenta las entregas reales por hora frente a las solicitadas.
- Implementa el flujo de permiso de fondo en dos pasos, con pantalla de justificación propia, y mide la tasa de aceptación frente a pedirlo de golpe.
- Envuelve
requestLocationUpdatesen un flujo frío que se desregistre al perder el último coleccionista y verifica condumpsysque no queda suscripción viva. - Sustituye una funcionalidad de proximidad basada en flujo continuo por
PRIORITY_PASSIVEy comprueba cuántos casos siguen cubiertos a coste cero.