wandres.dev
UBICACIÓN Y SENSORES · el mundo físico

El proveedor fusionado: qué es una posición y cuánto cuesta

Anatomía del FusedLocationProviderClient: cómo se combinan GNSS, redes y sensores inerciales en una única estimación, qué significa realmente el radio de precisión, en qué se diferencian la ubicación aproximada y la exacta desde Android 12, y por qué cada modo tiene un coste energético distinto y perfectamente predecible.

⏱ 18 min

Preguntar dónde está un teléfono parece una operación de lectura, como consultar el nivel de batería o la hora del sistema. No lo es. Una posición no se lee: se estima, se fusiona y se paga. Detrás de cada objeto Location hay un receptor de radiofrecuencia escuchando satélites a veinte mil kilómetros, una base de datos de puntos de acceso inalámbricos, un modelo de propagación celular y un filtro que reconcilia todo eso en tiempo real. El resultado no es un punto: es una distribución de probabilidad comprimida en tres números. Entender esa diferencia es la frontera entre una aplicación que usa la ubicación y una que la respeta.

🎯 Al terminar esta lección sabrás
  • Describir qué fuentes fusiona el FusedLocationProviderClient y con qué criterio las pondera.
  • Interpretar correctamente la semántica estadística del campo accuracy de un Location.
  • Distinguir ACCESS_COARSE_LOCATION de ACCESS_FINE_LOCATION y el degradado que impone el usuario desde Android 12.
  • Razonar el coste energético de cada fuente para elegir el modo mínimo suficiente.

Qué fusiona realmente el proveedor fusionado

El nombre es literal. El FusedLocationProviderClient de los servicios de Google Play no es un proveedor más junto al GNSS y a la red: es un estimador que consume a los demás y publica una única hipótesis. Sus entradas son heterogéneas en precisión, latencia y consumo, y la fusión existe precisamente para explotar esa heterogeneidad.

El receptor GNSS, que en un terminal moderno rastrea de forma simultánea las constelaciones GPS, Galileo, GLONASS y BeiDou, ofrece la mejor precisión absoluta a cielo abierto, del orden de tres a cinco metros, y bastante mejor en los equipos con doble frecuencia. A cambio exige visión directa del cielo, tarda decenas de segundos en un arranque en frío mientras descarga las efemérides, y es con diferencia la fuente más cara en energía. La triangulación por puntos de acceso inalámbricos y por identificadores de celda resuelve en menos de un segundo consultando una base de datos de emplazamientos conocidos, con precisiones que van de veinte metros en zona urbana densa a varios kilómetros en el campo, y con un coste marginal si la radio ya estaba encendida. Los sensores inerciales (acelerómetro, giroscopio y magnetómetro) no localizan por sí solos, pero permiten propagar la última posición conocida durante los huecos mediante navegación por estima, y su consumo se mide en microamperios porque los ejecuta un coprocesador de bajo consumo, no la CPU principal.

La fusión es, conceptualmente, un filtro bayesiano: mantiene un estado con su incertidumbre y lo actualiza con cada observación, ponderando cada fuente por la fiabilidad que declara. Una consecuencia práctica y contraintuitiva es que el sistema puede devolver una posición mejor que la de cualquiera de sus entradas por separado, y también que dos aplicaciones que piden ubicación a la vez comparten el trabajo: el proveedor amortiza una única adquisición entre todos los clientes activos.

flowchart TD
A[GNSS multiconstelacion] --> F[filtro de fusion]
B[WiFi y celdas] --> F
C[sensores inerciales] --> F
D[historial y modelos] --> F
F --> G[Location con latitud longitud y accuracy]
G --> H[clientes de la app]
style F fill:#89b4fa,color:#11111b
style G fill:#a6e3a1,color:#11111b
ℹ️
accuracy no es un margen de error, es un radio de confianza

El campo accuracy de un Location se documenta como el radio, en metros, de un círculo centrado en la posición reportada dentro del cual se encuentra la posición real con un 68 por ciento de probabilidad, es decir, una desviación típica. Leerlo como una cota dura es un error de diseño con consecuencias visibles: significa que aproximadamente una de cada tres lecturas cae fuera de ese círculo, y que existe una cola larga de lecturas muy alejadas. Cualquier lógica que compare distancias, dispare avisos o pinte un marcador debe tratar accuracy como una escala, no como una garantía, y descartar por umbral las muestras cuya incertidumbre supera lo que la funcionalidad tolera.

Esa lectura probabilística se extiende al resto del objeto. La velocidad, el rumbo y la altitud traen cada uno su propia incertidumbre en campos separados —speedAccuracyMetersPerSecond, bearingAccuracyDegrees y verticalAccuracyMeters—, y la altitud merece una advertencia propia: el GNSS la estima con un error típico dos o tres veces mayor que el horizontal, y la refiere al elipsoide de referencia, no al nivel del mar, de modo que compararla con la cota de un mapa sin corregir la ondulación del geoide produce discrepancias de decenas de metros. El rumbo, por su parte, solo tiene sentido cuando hay desplazamiento real: con el dispositivo parado es ruido puro.

Queda un campo que ninguna aplicación con lógica sensible debería ignorar. El sistema marca las posiciones inyectadas por una aplicación de simulación, accesible mediante isMock en las versiones modernas, y su presencia es la señal de que alguien está falsificando la ubicación. Para un juego basado en desplazamiento, un control de presencia o una verificación de reparto, tratar esa marca es la diferencia entre un producto y un juguete.

Aproximada frente a exacta: dos permisos y un interruptor del usuario

Android modela la precisión como una decisión de privacidad, no como un parámetro técnico. El manifiesto declara ACCESS_COARSE_LOCATION para posición aproximada, con una granularidad que el sistema difumina a una rejilla del orden de kilómetros, o ACCESS_FINE_LOCATION para la posición exacta que el hardware sea capaz de producir.

Desde Android 12 la relación entre ambos dejó de ser jerárquica en la práctica. El diálogo de permisos ofrece al usuario dos botones —ubicación precisa y ubicación aproximada— incluso cuando la aplicación pidió la exacta, de modo que una concesión parcial es un resultado esperado y no un caso de error. Por eso ACCESS_FINE_LOCATION debe solicitarse siempre junto a ACCESS_COARSE_LOCATION en la misma llamada, y la respuesta debe inspeccionarse entrada por entrada.

val permisos = arrayOf(
    Manifest.permission.ACCESS_FINE_LOCATION,
    Manifest.permission.ACCESS_COARSE_LOCATION,
)

val lanzador = registerForActivityResult(
    ActivityResultContracts.RequestMultiplePermissions()
) { resultado ->
    when {
        resultado[Manifest.permission.ACCESS_FINE_LOCATION] == true -> modoPreciso()
        resultado[Manifest.permission.ACCESS_COARSE_LOCATION] == true -> modoAproximado()
        else -> sinUbicacion()
    }
}

Y el permiso no agota las condiciones. Por encima de él está el interruptor global de ubicación del dispositivo, y por debajo el estado concreto de los proveedores: el usuario puede tener la ubicación activada pero la precisión de Google desactivada, o estar en modo de ahorro de energía con el GNSS restringido. Comprobar ese estado antes de pedir nada, y ofrecer al usuario el diálogo del sistema que lo resuelve sin salir de la aplicación, evita la peor de las experiencias posibles, que es un indicador de carga girando indefinidamente sin explicación.

val ajustes = LocationSettingsRequest.Builder()
    .addLocationRequest(peticion)
    .setAlwaysShow(true)
    .build()

LocationServices.getSettingsClient(context)
    .checkLocationSettings(ajustes)
    .addOnFailureListener { error ->
        if (error is ResolvableApiException) {
            lanzadorAjustes.launch(IntentSenderRequest.Builder(error.resolution).build())
        }
    }

La consecuencia arquitectónica es que la aplicación necesita dos modos funcionales reales, no uno degradado a base de mensajes de error. Un buscador de tiendas cercanas funciona perfectamente con precisión aproximada; una navegación paso a paso, no. Diseñar el modo aproximado como ciudadano de primera clase es lo que evita que el usuario perciba la concesión parcial como un castigo, y con ello lo que evita que revoque el permiso por completo.

📍

Aproximada

Difuminada a una rejilla de kilómetros. Suficiente para clima, contenido regional, moneda o listados por ciudad. Casi siempre resuelta con radio ya encendida.

🎯

Exacta

Metros. Necesaria para navegación, actividad deportiva, recogida en punto concreto o mapas de detalle. Suele implicar GNSS activo.

🔁

Última conocida

getLastLocation devuelve la caché del sistema sin encender nada. Puede ser nula o vieja: hay que validar siempre su antigüedad.

⏱️

Bajo demanda

getCurrentLocation fuerza una lectura fresca con un CancellationToken. Es el punto medio correcto para acciones puntuales.

El coste energético de cada modo

El consumo no es una propiedad de la ubicación, sino de las radios que hay que despertar para obtenerla. Ese es el modelo mental que permite predecir la factura antes de escribir el código. Un getLastLocation es prácticamente gratuito porque solo lee una caché del sistema. Una posición de red aprovecha exploraciones que a menudo ya iban a ocurrir. Una posición GNSS, en cambio, mantiene un receptor de radiofrecuencia y su procesado de señal encendidos durante segundos, e impide que el procesador de aplicaciones entre en reposo profundo mientras dura la adquisición.

Tres factores multiplican ese coste base. El primero es la frecuencia: reducir el intervalo a la mitad no duplica exactamente el gasto, pero se acerca, porque cada ciclo paga un arranque. El segundo es el arranque en frío: sin efemérides recientes el receptor puede tardar decenas de segundos con la radio a plena potencia, lo que convierte una petición esporádica en algo mucho más caro que una periódica. El tercero es el entorno: en interiores o en un desfiladero urbano el receptor no consigue fijación y sigue consumiendo sin producir nada útil, el peor de los escenarios posibles.

val cliente = LocationServices.getFusedLocationProviderClient(context)

suspend fun posicionActual(): Location? {
    val cache = cliente.lastLocation.await()
    val antiguedad = SystemClock.elapsedRealtimeNanos() - (cache?.elapsedRealtimeNanos ?: 0)
    if (cache != null && antiguedad < TimeUnit.MINUTES.toNanos(2)) return cache

    return cliente.getCurrentLocation(
        Priority.PRIORITY_BALANCED_POWER_ACCURACY,
        CancellationTokenSource().token,
    ).await()
}

Ese patrón —consultar la caché, validar su antigüedad con elapsedRealtimeNanos en lugar de con la hora del sistema, y solo entonces pedir una lectura fresca con la prioridad mínima suficiente— resuelve la gran mayoría de los casos de uso puntuales con un coste cercano a cero. La marca temporal monótona importa: el reloj de pared puede saltar hacia atrás por sincronización de red y producir antigüedades negativas o absurdas.

📝
El arranque en frío es el gasto que nadie contabiliza

Un receptor GNSS necesita conocer las efemérides —las órbitas precisas de los satélites— para calcular una posición. Si las tiene recientes, la fijación llega en uno o dos segundos; si están caducadas, debe demodularlas de la propia señal satelital a cincuenta bits por segundo, lo que en el peor caso supera el medio minuto con la radio a plena potencia y sin producir ningún resultado intermedio. Por eso una petición esporádica aislada puede consumir más que varias peticiones seguidas, y por eso los servicios de asistencia que descargan esos datos por red son determinantes para el tiempo hasta la primera fijación. La consecuencia de diseño es contraintuitiva: agrupar la necesidad de posición en ráfagas cortas suele salir más barato que repartirla en lecturas muy espaciadas, porque cada lectura espaciada vuelve a pagar el arranque.

Conviene además tener presente que el proveedor fusionado no es la única superficie. Cuando la aplicación necesita el dato crudo —una medición científica, un análisis de calidad de señal, un cálculo propio— la API de medidas GNSS expone pseudodistancias, relación señal a ruido y estado de cada satélite. Es una herramienta de nicho, mucho más cara y sin ninguna fusión que la amortigüe, pero es la única vía cuando la pregunta no es dónde estoy sino por qué la estimación es tan mala aquí.

Una posición es una hipótesis con factura, no un dato

Todo lo que sigue en este nivel se deriva de una sola reconceptualización: la ubicación no es un atributo que el dispositivo posea y tú consultes, sino una inferencia que el sistema construye bajo demanda, con un coste energético real y una incertidumbre que no puede eliminar. De ahí salen las tres disciplinas del desarrollador experto. La primera es tratar la incertidumbre como parte del contrato: si accuracy viene con cien metros, la aplicación no tiene derecho a afirmar que el usuario está en un portal concreto, y toda la interfaz debe reflejar esa duda en lugar de esconderla tras un marcador engañosamente puntual. La segunda es tratar la energía como recurso compartido: cada fijación que pides se la quitas al resto del sistema, y el usuario percibe la suma, no tu parte. La tercera, y la más difícil, es aceptar que la precisión es una petición, nunca una garantía: el usuario puede concederte solo la aproximada, el sistema puede degradarte en ahorro de energía, y el entorno puede negarte la fijación durante minutos. Un código que asume el caso bueno se rompe en el metro, en un aparcamiento subterráneo y en el primer usuario que toca el botón de aproximada. La pregunta correcta antes de cada petición no es qué precisión quiero, sino cuál es la peor precisión con la que esta funcionalidad sigue siendo honesta. Casi siempre la respuesta es mucho peor de lo que el código pedía, y ahí es donde vive la eficiencia.

⚔️ Mide la factura de una posición
  1. Registra la fuente, la accuracy y la latencia de cien lecturas en interior y otras cien a cielo abierto; compara las distribuciones, no las medias.
  2. Implementa el patrón caché con validación por elapsedRealtimeNanos y mide qué porcentaje de peticiones evita encender el GNSS.
  3. Fuerza en el diálogo del sistema la concesión de solo ubicación aproximada y verifica que tu aplicación entra en un modo funcional completo, sin mensajes de error.
  4. Compara con Battery Historian el consumo de una lectura puntual con PRIORITY_HIGH_ACCURACY frente a PRIORITY_BALANCED_POWER_ACCURACY.
  5. Dibuja en el mapa el círculo de accuracy junto al marcador y observa cuántas veces la posición real habría quedado fuera.