Si eres un fabricante de equipos originales (OEM) o mantienes un cliente de actualización inalámbrica (OTA) privilegiado, puedes brindar a las apps sensibles a la seguridad del dispositivo visibilidad de las actualizaciones de seguridad pendientes para que puedan evaluar con precisión la postura de seguridad del dispositivo. Para aplicar políticas sólidas de confianza cero, las apps deben poder verificar no solo el nivel de parche instalado en el dispositivo (nivel de parche de seguridad del dispositivo o DSPL), sino también qué actualizaciones de seguridad están disponibles y listas para instalarse (nivel de parche de seguridad disponible o ASPL).
Debido a que las apps cliente sin privilegios no pueden leer directamente las propiedades del firmware, inspeccionar las bases de datos privadas del actualizador ni consultar los extremos internos del backend del OEM, la biblioteca de AndroidX Security State Provider proporciona una arquitectura de comunicación entre procesos (IPC) estandarizada y segura que los clientes de actualización usan para compartir información sobre las actualizaciones disponibles. Si implementas un UpdateInfoService en tu cliente de actualización OTA, puedes publicar metadatos de ASPL para el sistema sin exponer integraciones de backend propietarias. Si bien Google proporciona la implementación de actualizadores de componentes modulares del sistema (Mainline) para dispositivos GMS, los dispositivos que no son GMS también pueden publicar metadatos de ASPL para estos componentes modulares del sistema.
Descripción general de la arquitectura
En el siguiente diagrama, se ilustra cómo la biblioteca de AndroidX Security State Provider establece un marco de IPC estandarizado y seguro entre las apps cliente sin privilegios y los servicios de actualización integrados en el dispositivo:

Modelos de entrega de datos
Las apps cliente consultan la disponibilidad de actualizaciones llamando a queryAllAvailableUpdates o fetchAvailableSecurityPatchLevel. En segundo plano, la biblioteca cliente descubre automáticamente todos los servicios registrados que extienden la clase UpdateInfoService en el dispositivo desde las apps del sistema que tienen el permiso READ_PRIVILEGED_PHONE_STATE y se vincula a ellos.
Como se ilustra en el diagrama anterior, la biblioteca de security-state-provider admite dos modelos de entrega de datos:
| Modelo de entrega | Activador de sincronización | Respuesta del cliente | Casos de uso recomendados |
|---|---|---|---|
| Modelo de envío (sincronización en segundo plano) | Los trabajadores en segundo plano programados (WorkManager o JobScheduler) se sincronizan con tu backend y escriben registros en UpdateInfoManager. Tu servicio siempre se entrega desde la memoria caché del disco local (shouldFetchUpdates() = false). |
Se entrega de inmediato desde la caché local. | Actualizadores OTA del sistema del OEM y actualizadores de componentes modulares sincronizados en segundo plano. |
| Modelo de extracción (sincronización on demand) | Las consultas de IPC del cliente entrantes activan una recuperación de red cuando los registros almacenados en caché están desactualizados (shouldFetchUpdates() = true). La combinación de mutex y la limitación de frecuencia (shouldThrottle()) protegen tu backend de los picos. |
Espera la recuperación del backend cuando la caché está inactiva. | Actualizadores OTA monolíticos del OEM sin trabajadores de sincronización en segundo plano programados. |
Varios proveedores de actualizaciones
En los dispositivos Android de producción, coexisten varios proveedores de actualizaciones independientes de forma simultánea. Por ejemplo, Mainline publica la disponibilidad de los componentes modulares (COMPONENT_SYSTEM_MODULES), mientras que tu cliente de OTA del OEM publica actualizaciones para la imagen del SO principal (COMPONENT_SYSTEM).
Tu servicio solo necesita registrar actualizaciones para los componentes específicos que administra. Si varios proveedores en un dispositivo publican actualizaciones para el mismo componente, las apps cliente evalúan el nivel de parche disponible más alto (con fetchAvailableSecurityPatchLevel()) o inspeccionan registros UpdateInfo individuales (con queryAllAvailableUpdates()) para la auditoría empresarial. Asegúrate de que tu servicio siempre publique el formato canónico de tu componente (DateBasedSecurityPatchLevel para COMPONENT_SYSTEM).
Guía paso a paso para integrar tu cliente de actualizaciones
Sigue estos pasos para integrar la biblioteca de AndroidX Security State Provider en tu cliente de actualización y comenzar a publicar la disponibilidad de las actualizaciones de seguridad de tu dispositivo.
Paso 1: Agrega dependencias
Para implementar un proveedor de actualizaciones, asegúrate de que tu proyecto incluya el repositorio de Maven de Google y, luego, agrega la biblioteca security-state-provider al archivo build.gradle.kts (Kotlin DSL) o build.gradle (Groovy DSL) de tu módulo:
Kotlin
// Kotlin DSL (build.gradle.kts)
dependencies {
// Core provider library for OTA and system update clients
implementation("androidx.security:security-state-provider:1.0.0")
// Required to construct UpdateInfo and DateBasedSecurityPatchLevel records
implementation("androidx.security:security-state:1.1.0")
// Optional: Guava ListenableFuture support for Java implementations
implementation("androidx.concurrent:concurrent-futures:1.2.0")
implementation("com.google.guava:guava:33.0.0-android")
}
Groovy
// Groovy DSL (build.gradle)
dependencies {
// Core provider library for OTA and system update clients
implementation 'androidx.security:security-state-provider:1.0.0'
// Required to construct UpdateInfo and DateBasedSecurityPatchLevel records
implementation 'androidx.security:security-state:1.1.0'
// Optional: Guava ListenableFuture support for Java implementations
implementation 'androidx.concurrent:concurrent-futures:1.2.0'
implementation 'com.google.guava:guava:33.0.0-android'
}
Paso 2: Declara el servicio de actualización en tu manifiesto
Declara tu servicio en el AndroidManifest.xml de tu app con un <intent-filter> que coincida con androidx.security.state.provider.UPDATE_INFO_SERVICE. El servicio debe exportarse (android:exported="true") y configurarse como un servicio de un solo usuario (android:singleUser="true") para que la biblioteca cliente pueda vincularse a él a través de los límites del proceso y del usuario, en especial para los perfiles de trabajo:
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.android.updater">
<application>
<service
android:name=".MyUpdateInfoService"
android:exported="true"
android:singleUser="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="androidx.security.state.provider.UPDATE_INFO_SERVICE" />
</intent-filter>
</service>
</application>
</manifest>
Si tu actualizador no se ejecuta como android.uid.system, también debes declarar los siguientes permisos en tu manifiesto y asegurarte de agregarlos a tu lista de entidades permitidas de permisos privilegiados:
READ_PRIVILEGED_PHONE_STATE: Se requiere para que los clientes confíen en tu proveedor.INTERACT_ACROSS_USERS: Se requiere paraandroid:singleUser="true".
Paso 3: Implementa UpdateInfoService
Para publicar el estado de actualización, debes implementar la clase UpdateInfoService y crear registros de actualización que coincidan con el modelo de datos esperado.
Especificación del modelo de datos de UpdateInfo
Ya sea que elijas el modelo Push o el modelo Pull, construye registros UpdateInfo con UpdateInfo.Builder según la siguiente especificación:
| Nombre del campo | Método getter | Tipo de dato | Requisitos de validación y formato | Semántica del sistema y del propósito |
|---|---|---|---|---|
component |
getComponent() |
String (@Component) |
Constantes canónicas en SecurityPatchState: COMPONENT_SYSTEM, COMPONENT_SYSTEM_MODULES o COMPONENT_KERNEL. |
Identifica el subsistema de software o firmware al que se dirige esta actualización. |
securityPatchLevel |
getSecurityPatchLevel() |
SecurityPatchLevel |
Debe ser una instancia de DateBasedSecurityPatchLevel (YYYY-MM-DD) o VersionedSecurityPatchLevel (major.minor.patch), o bien se debe analizar con SecurityPatchState.getComponentSecurityPatchLevel(). |
Es el nivel de parche de seguridad objetivo que se alcanzará una vez que se instale esta actualización. |
publishedDateMillis |
getPublishedDateMillis() |
long |
Milisegundos desde la época de Unix (System.currentTimeMillis()). Debe ser > 0. |
Fecha y hora en que la actualización estuvo disponible para los usuarios, como la hora de lanzamiento de la OTA. No uses el tiempo de descarga o instalación de la carga útil. |
lastCheckTimeMillis |
getLastCheckTimeMillis() |
long |
Milisegundos desde la época de Unix. Debe ser > 0. |
Es la marca de tiempo en la que tu proveedor verificó o descubrió este registro de actualización durante la sincronización. |
Elige el modelo de entrega que se adapte a la arquitectura de tu actualizador entre las siguientes opciones:
Opción A: Modelo push (recomendado)
Cuando el worker de sincronización en segundo plano verifique tu servidor OTA, valida que cualquier actualización descubierta avance el nivel de parche actual del dispositivo y persístelo con UpdateInfoManager.registerUpdate(), o llama a UpdateInfoManager.unregisterUpdate() si no hay ninguna actualización de seguridad pendiente. Siempre llama a UpdateInfoManager.setLastCheckTimeMillis() al final de cada sincronización (incluso después de llamar a registerUpdate(), que persiste el registro UpdateInfo por componente, pero no actualiza la marca de tiempo global de la última verificación que se devuelve a los clientes). Puedes implementar esto con un WorkManager CoroutineWorker en Kotlin o Worker en Java:
Kotlin
import android.content.Context
import androidx.security.state.SecurityPatchState
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel
import androidx.security.state.UpdateInfo
import androidx.security.state.provider.UpdateInfoManager
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import kotlin.math.max
class OtaSyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val updateInfoManager = UpdateInfoManager(applicationContext)
val securityPatchState = SecurityPatchState(applicationContext)
val currentSpl = securityPatchState.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM)
// 1. Fetch available update metadata from OEM backend
val latestUpdate = MyOtaClient.fetchLatestSystemUpdate()
val targetSplString = latestUpdate?.spl?.trim()
val targetSpl = if (!targetSplString.isNullOrEmpty()) {
DateBasedSecurityPatchLevel.fromString(targetSplString)
} else {
null
}
// 2. Defensively verify that target SPL is non-blank AND strictly newer than installed DSPL.
// If an update is a maintenance patch with no SPL increment (or if no update is available),
// unregister any stale cached record for this component.
if (latestUpdate != null && targetSpl != null && targetSpl > currentSpl) {
val updateInfo = UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(targetSpl)
.setPublishedDateMillis(latestUpdate.releaseTimeMillis)
.setLastCheckTimeMillis(System.currentTimeMillis())
.build()
updateInfoManager.registerUpdate(updateInfo)
} else {
val clearTarget = UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build()
updateInfoManager.unregisterUpdate(clearTarget)
}
// 3. Update global freshness timestamp (monotonic synchronization)
val currentCheckTime = System.currentTimeMillis()
val previousCheckTime = updateInfoManager.getLastCheckTimeMillis()
updateInfoManager.setLastCheckTimeMillis(max(previousCheckTime, currentCheckTime))
return Result.success()
}
}
Java
import android.content.Context;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.security.state.SecurityPatchState;
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel;
import androidx.security.state.SecurityPatchState.SecurityPatchLevel;
import androidx.security.state.UpdateInfo;
import androidx.security.state.provider.UpdateInfoManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
public class OtaSyncWorker extends Worker {
public OtaSyncWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
}
@NonNull
@Override
public Result doWork() {
// In Java, pass null for customSecurityState because UpdateInfoManager does not declare @JvmOverloads
UpdateInfoManager updateInfoManager =
new UpdateInfoManager(getApplicationContext(), /* customSecurityState= */ null);
SecurityPatchState securityPatchState = new SecurityPatchState(getApplicationContext());
SecurityPatchLevel currentSpl =
securityPatchState.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM);
// 1. Fetch available update metadata from OEM backend
MyOtaUpdate latestUpdate = MyOtaClient.fetchLatestSystemUpdate();
String targetSplString = (latestUpdate != null && latestUpdate.getSpl() != null)
? latestUpdate.getSpl().trim()
: null;
DateBasedSecurityPatchLevel targetSpl =
!TextUtils.isEmpty(targetSplString)
? DateBasedSecurityPatchLevel.fromString(targetSplString)
: null;
// 2. Defensively verify that target SPL is non-blank AND strictly newer than installed DSPL.
// If an update is a maintenance patch with no SPL increment (or if no update is available),
// unregister any stale cached record for this component.
if (latestUpdate != null && targetSpl != null && targetSpl.compareTo(currentSpl) > 0) {
UpdateInfo updateInfo = new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(targetSpl)
.setPublishedDateMillis(latestUpdate.getReleaseTimeMillis())
.setLastCheckTimeMillis(System.currentTimeMillis())
.build();
updateInfoManager.registerUpdate(updateInfo);
} else {
UpdateInfo clearTarget = new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build();
updateInfoManager.unregisterUpdate(clearTarget);
}
// 3. Update global freshness timestamp (monotonic synchronization)
long currentCheckTime = System.currentTimeMillis();
long previousCheckTime = updateInfoManager.getLastCheckTimeMillis();
updateInfoManager.setLastCheckTimeMillis(Math.max(previousCheckTime, currentCheckTime));
return Result.success();
}
}
En un modelo basado en la transmisión, las tareas en segundo plano conservan los registros de actualización directamente en UpdateInfoManager. Para indicarle al framework que siempre publique registros desde el almacenamiento en disco local, anula shouldFetchUpdates() para que devuelva false extendiendo UpdateInfoService en Kotlin o ListenableFutureUpdateInfoService en Java:
Kotlin
import androidx.security.state.UpdateInfo
import androidx.security.state.provider.UpdateInfoService
class PushUpdateInfoService : UpdateInfoService() {
// Cache is populated out-of-band by background sync tasks
override fun shouldFetchUpdates(): Boolean = false
// Never invoked under normal flow because shouldFetchUpdates() returns false
override suspend fun fetchUpdates(): List<UpdateInfo> = emptyList()
}
Java
import androidx.annotation.NonNull;
import androidx.security.state.UpdateInfo;
import androidx.security.state.provider.ListenableFutureUpdateInfoService;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.Collections;
import java.util.List;
public class PushUpdateInfoService extends ListenableFutureUpdateInfoService {
@Override
protected boolean shouldFetchUpdates() {
return false;
}
@NonNull
@Override
protected ListenableFuture<List<UpdateInfo>> fetchUpdatesAsync() {
return Futures.immediateFuture(Collections.emptyList());
}
}
Opción B: Modelo de extracción (a pedido)
En una arquitectura basada en extracción, tu servicio controla las solicitudes de actualización a pedido que activan las apps cliente cuando la caché local está desactualizada.
Para controlar las consultas de actualización a pedido, extiende UpdateInfoService en Kotlin (implementa la función de suspensión fetchUpdates()) o ListenableFutureUpdateInfoService en Java (implementa fetchUpdatesAsync() que devuelve un ListenableFuture de Guava):
Kotlin
package com.example.android.updater
import androidx.security.state.SecurityPatchState
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel
import androidx.security.state.UpdateInfo
import androidx.security.state.provider.UpdateInfoManager
import androidx.security.state.provider.UpdateInfoService
import java.util.concurrent.TimeUnit
class MyUpdateInfoService : UpdateInfoService() {
// Manage local update records and check timestamps
private val updateInfoManager by lazy { UpdateInfoManager(this) }
override suspend fun fetchUpdates(): List<UpdateInfo> {
val currentSpl = SecurityPatchState(this)
.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM)
// 1. Execute network request to OTA backend
val response = MyOtaBackendClient.checkAvailableUpdates()
// 2. Defensively filter out blank or non-advancing SPLs and map to UpdateInfo objects
val validUpdates = response.updates
.mapNotNull { updateItem ->
val splString = updateItem.targetSpl?.trim()
if (splString.isNullOrEmpty()) return@mapNotNull null
val parsedSpl = DateBasedSecurityPatchLevel.fromString(splString)
if (parsedSpl > currentSpl) {
UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(parsedSpl)
.setPublishedDateMillis(updateItem.releaseTimestampMillis)
.setLastCheckTimeMillis(System.currentTimeMillis())
.build()
} else {
null
}
}
// 3. If no advancing SYSTEM update is available (or if a previously offered update was revoked),
// proactively unregister any cached record for this component.
if (validUpdates.isEmpty()) {
val clearTarget = UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build()
updateInfoManager.unregisterUpdate(clearTarget)
}
return validUpdates
}
override fun shouldFetchUpdates(): Boolean {
// Enforce custom freshness threshold (for example, 4 hours instead of default 1 hour)
val lastCheckMillis = updateInfoManager.getLastCheckTimeMillis()
val dataAge = System.currentTimeMillis() - lastCheckMillis
return dataAge > TimeUnit.HOURS.toMillis(4)
}
}
Java
package com.example.android.updater;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.security.state.SecurityPatchState;
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel;
import androidx.security.state.SecurityPatchState.SecurityPatchLevel;
import androidx.security.state.UpdateInfo;
import androidx.security.state.provider.ListenableFutureUpdateInfoService;
import androidx.security.state.provider.UpdateInfoManager;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class MyUpdateInfoService extends ListenableFutureUpdateInfoService {
private UpdateInfoManager updateInfoManager;
@Override
public void onCreate() {
super.onCreate();
// Pass null for customSecurityState because UpdateInfoManager does not declare @JvmOverloads
updateInfoManager = new UpdateInfoManager(this, /* customSecurityState= */ null);
}
@NonNull
@Override
protected ListenableFuture<List<UpdateInfo>> fetchUpdatesAsync() {
try {
SecurityPatchLevel currentSpl = new SecurityPatchState(this)
.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM);
MyOtaBackendResponse response = MyOtaBackendClient.checkAvailableUpdates();
List<UpdateInfo> updates = new ArrayList<>();
for (MyOtaUpdateItem item : response.getUpdates()) {
String trimmedSpl = (item.getTargetSpl() != null) ? item.getTargetSpl().trim() : null;
if (!TextUtils.isEmpty(trimmedSpl)) {
DateBasedSecurityPatchLevel parsedSpl =
DateBasedSecurityPatchLevel.fromString(trimmedSpl);
if (parsedSpl.compareTo(currentSpl) > 0) {
updates.add(new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(parsedSpl)
.setPublishedDateMillis(item.getReleaseTimestampMillis())
.setLastCheckTimeMillis(System.currentTimeMillis())
.build());
}
}
}
// If no advancing SYSTEM update is available (or if a previously offered update was revoked),
// proactively unregister any cached record for this component.
if (updates.isEmpty()) {
UpdateInfo clearTarget = new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build();
updateInfoManager.unregisterUpdate(clearTarget);
}
return Futures.immediateFuture(updates);
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}
}
@Override
protected boolean shouldFetchUpdates() {
long lastCheckMillis = updateInfoManager.getLastCheckTimeMillis();
long dataAge = System.currentTimeMillis() - lastCheckMillis;
return dataAge > TimeUnit.HOURS.toMillis(4);
}
}
Paso 4: Borra las actualizaciones aplicadas después de reiniciar el dispositivo
Si bien UpdateInfoManager poda automáticamente las actualizaciones obsoletas cada vez que se invoca registerUpdate(), tu actualizador no volverá a llamar a registerUpdate() después de que finalice la instalación de una actualización OTA hasta el próximo ciclo de sincronización programado del servidor. Para evitar que las apps cliente vean una actualización ya instalada como pendiente inmediatamente después del reinicio, escucha ACTION_BOOT_COMPLETED y llama a UpdateInfoManager.unregisterUpdate() cuando finalice la instalación de una actualización OTA para borrar el registro de la caché local. Hacer esto solo cuando finaliza la instalación de una actualización evita borrar incondicionalmente las actualizaciones pendientes (sin instalar) en cada reinicio normal del dispositivo. Dado que las claves de UpdateInfoManager actualizan los registros por componente, solo debes especificar el componente de destino cuando construyas el objeto UpdateInfo para anular el registro:
Kotlin
// Build target identifying the component to unregister
val target = UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build()
// Unregister the update to remove it from disk cache
updateInfoManager.unregisterUpdate(target)
// Refresh last check timestamp to indicate up-to-date state
updateInfoManager.setLastCheckTimeMillis(System.currentTimeMillis())
Java
// Build target identifying the component to unregister
UpdateInfo target = new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build();
// Unregister the update to remove it from disk cache
updateInfoManager.unregisterUpdate(target);
// Refresh last check timestamp to indicate up-to-date state
updateInfoManager.setLastCheckTimeMillis(System.currentTimeMillis());
Paso 5: Verifica tu integración
Ejecuta las siguientes verificaciones en un dispositivo Android o emulador con Android Debug Bridge (ADB) para validar la integración de extremo a extremo y evitar errores comunes en la implementación de OEM:
Verifica que los clientes confíen en tu proveedor: Las apps cliente ignoran a cualquier proveedor que no tenga
READ_PRIVILEGED_PHONE_STATE, incluso si está preinstalado. Confirma que se haya otorgado el permiso:adb shell dumpsys package <your_package_name> | grep "READ_PRIVILEGED_PHONE_STATE: granted=true"Luego, confirma que se pueda detectar tu servicio y que no tenga permisos de servicio. En el resultado de tu servicio, busca
exported=trueypermission=null:adb shell pm query-services --user 0 -a androidx.security.state.provider.UPDATE_INFO_SERVICESi un cliente aún no ve tu proveedor, verifica si hay
Ignoring untrusted update provideren logcat desde la etiquetaSecurityPatchState.Verifica la resolución de intents en el usuario 0 y en los perfiles de trabajo: Asegúrate de que el SO Android
PackageManagerresuelva tu filtro de intentsUPDATE_INFO_SERVICEexportado en el usuario principal (User 0) y en cualquier perfil de trabajo de Android Enterprise activo (comoUser 10):adb shell pm query-services --user 0 -a androidx.security.state.provider.UPDATE_INFO_SERVICE adb shell pm query-services --user 10 -a androidx.security.state.provider.UPDATE_INFO_SERVICEVerifica el estado del servicio y los registros almacenados en caché con
dumpsys:UpdateInfoServiceanuladump()para informarGlobal Last Check,Should Throttle(el estado del limitador de frecuencia) yCached Updates. Dado queUpdateInfoServicees un servicio vinculado y los clientes desvinculan inmediatamente después de realizar la consulta,dumpsys activity servicegenera(nothing)cuando no hay ningún cliente vinculado. Inicia el servicio de forma explícita antes de ejecutardumpsys:adb shell am start-service -a androidx.security.state.provider.UPDATE_INFO_SERVICE <your_package_name>/.<service_class_name> adb shell dumpsys activity service <your_package_name>/.<service_class_name>Ejemplo de resultado del diagnóstico:
UpdateInfoService State: Active Requests: 0 Global Last Check: Thu Jan 01 12:00:00 UTC 2026 Should Throttle: false Cached Updates (1): - Component: SYSTEM SPL: 2026-01-01 Published: Thu Jan 01 00:00:00 UTC 2026 Last Checked: Thu Jan 01 12:00:00 UTC 2026Activa la vinculación del cliente y verifica los resultados de la telemetría: Desde una app de prueba sin privilegios (que no tiene permisos de firma del sistema), invoca
SecurityPatchState.queryAllAvailableUpdates(). Si implementaste las devoluciones de llamada de telemetría, verifica lo siguiente:- Verifica que el cliente sin privilegios se vincule sin un
SecurityExceptiony activeonClientConnected(packageName, callerUid). - Para los proveedores del modelo de envío (
shouldFetchUpdates() == false): Verifica queonRequestCompleted(telemetry)registreUpdateFetchOutcome.CACHE_HIT(1) confetchDurationMillis == 0en cada búsqueda. - Para los proveedores de modelos de extracción (
shouldFetchUpdates() == true): Verifica queonRequestCompleted(telemetry)registreUpdateFetchOutcome.FETCHED(3) en la consulta inicial de caché obsoleta, seguida deCACHE_HIT(1) en las consultas posteriores inmediatas. (Para restablecer el límite de frecuencia persistente de 1 hora entre las ejecuciones de prueba del modelo de extracción, ejecutaadb shell pm clear <your_package_name>).
- Verifica que el cliente sin privilegios se vincule sin un
Configuración opcional y avanzada
Política de almacenamiento en caché y límite de frecuencia
Cuando un cliente consulta actualizaciones, UpdateInfoService ejecuta un flujo de trabajo de bloqueo verificado dos veces para equilibrar la actualización de los datos con la carga del servidor de backend:

- Ruta rápida (
shouldFetchUpdates()): De forma predeterminada,shouldFetchUpdates()devuelvetrue(lo que indica una caché obsoleta) solo cuando ellastCheckTimeMillisglobal tiene más de 1 hora (TimeUnit.HOURS.toMillis(1)). CuandoshouldFetchUpdates()devuelvefalse, el servicio devuelve de inmediato los registros almacenados en caché con el resultadoUpdateFetchOutcome.CACHE_HITsin adquirir bloqueos ni realizar E/S de red. Puedes anularshouldFetchUpdates()para personalizar esta política de almacenamiento en caché. - Ruta lenta y coalescencia de solicitudes: Cuando
shouldFetchUpdates()devuelvetrue, el servicio adquiere un mutex de corrutina interno y vuelve a evaluarshouldFetchUpdates()(devuelveUpdateFetchOutcome.COALESCEDsi una solicitud simultánea ya actualizó la caché mientras esperaba el bloqueo). - Limitador de frecuencia persistente (
shouldThrottle()): Para proteger la infraestructura de backend de las ráfagas de consultas o las fallas repetidas,shouldThrottle()aplica un intervalo mínimo persistente de 1 hora en los reinicios de la app y el dispositivo.UpdateInfoServiceregistra cada intento antes de invocarfetchUpdates(), por lo que, sifetchUpdates()arroja una excepción (devuelveUpdateFetchOutcome.FAILEDdespués de invocaronFetchFailed(e)), las consultas posteriores durante los próximos 60 minutos devolverán con elegancia datos de resguardo almacenados en caché con el resultadoUpdateFetchOutcome.THROTTLED.
Observabilidad, telemetría y diagnóstico
UpdateInfoService proporciona hooks de observabilidad integrados para hacer un seguimiento de la adopción del cliente, supervisar la latencia de la IPC y registrar errores de backend sin instrumentar los stubs de AIDL de bajo nivel:
onRequestCompleted(telemetry): Se invoca cuando se completa cada verificación de actualización con un resumen deUpdateCheckTelemetry.onClientConnected(packageName, callerUid): Se invoca cuando un cliente verificado abre una sesión.onClientDisconnected(packageName, callerUid): Se invoca cuando un cliente desvincula o finaliza su proceso.onFetchFailed(e): Se invoca si se produce una excepción durantefetchUpdates(), antes de que el servicio devuelva datos de resguardo almacenados en caché.
Anula estas devoluciones de llamada en UpdateInfoService (Kotlin) o ListenableFutureUpdateInfoService (Java):
Kotlin
import androidx.security.state.provider.UpdateCheckTelemetry
import androidx.security.state.provider.UpdateFetchOutcome
import androidx.security.state.provider.UpdateInfoService
abstract class MonitoredUpdateInfoService : UpdateInfoService() {
override fun onRequestCompleted(telemetry: UpdateCheckTelemetry) {
val outcomeName = when (telemetry.outcome) {
UpdateFetchOutcome.CACHE_HIT -> "CACHE_HIT"
UpdateFetchOutcome.COALESCED -> "COALESCED"
UpdateFetchOutcome.FETCHED -> "FETCHED"
UpdateFetchOutcome.THROTTLED -> "THROTTLED"
UpdateFetchOutcome.FAILED -> "FAILED"
else -> "UNKNOWN"
}
MyAnalytics.logEvent("SECURITY_UPDATE_CHECK")
.addParam("outcome", outcomeName)
.addParam("total_duration_ms", telemetry.totalDurationMillis)
.addParam("lock_wait_ms", telemetry.lockWaitDurationMillis)
.addParam("processing_ms", telemetry.processingDurationMillis)
.addParam("fetch_duration_ms", telemetry.fetchDurationMillis)
.addParam("caller_uid", telemetry.callerUid)
.send()
}
override fun onClientConnected(packageName: String, callerUid: Int) {
// Track authenticated client sessions and adoption
MyMetrics.incrementCounter("client_connected", "package", packageName)
}
override fun onClientDisconnected(packageName: String, callerUid: Int) {
// Track session termination and cleanup resources
MyMetrics.incrementCounter("client_disconnected", "package", packageName)
}
override fun onFetchFailed(e: Exception) {
// Report exceptions caught during the update check workflow
MyCrashReporter.recordException(e)
}
}
Java
import androidx.annotation.NonNull;
import androidx.security.state.provider.ListenableFutureUpdateInfoService;
import androidx.security.state.provider.UpdateCheckTelemetry;
import androidx.security.state.provider.UpdateFetchOutcome;
public abstract class MonitoredUpdateInfoService extends ListenableFutureUpdateInfoService {
@Override
protected void onRequestCompleted(@NonNull UpdateCheckTelemetry telemetry) {
String outcomeName;
switch (telemetry.getOutcome()) {
case UpdateFetchOutcome.CACHE_HIT: outcomeName = "CACHE_HIT"; break;
case UpdateFetchOutcome.COALESCED: outcomeName = "COALESCED"; break;
case UpdateFetchOutcome.FETCHED: outcomeName = "FETCHED"; break;
case UpdateFetchOutcome.THROTTLED: outcomeName = "THROTTLED"; break;
case UpdateFetchOutcome.FAILED: outcomeName = "FAILED"; break;
default: outcomeName = "UNKNOWN"; break;
}
MyAnalytics.logEvent("SECURITY_UPDATE_CHECK")
.addParam("outcome", outcomeName)
.addParam("total_duration_ms", telemetry.getTotalDurationMillis())
.addParam("lock_wait_ms", telemetry.getLockWaitDurationMillis())
.addParam("processing_ms", telemetry.getProcessingDurationMillis())
.addParam("fetch_duration_ms", telemetry.getFetchDurationMillis())
.addParam("caller_uid", telemetry.getCallerUid())
.send();
}
@Override
protected void onClientConnected(@NonNull String packageName, int callerUid) {
MyMetrics.incrementCounter("client_connected", "package", packageName);
}
@Override
protected void onClientDisconnected(@NonNull String packageName, int callerUid) {
MyMetrics.incrementCounter("client_disconnected", "package", packageName);
}
@Override
protected void onFetchFailed(@NonNull Exception e) {
MyCrashReporter.recordException(e);
}
}
Resultados de la telemetría y métricas de latencia
UpdateCheckTelemetry mide las duraciones transcurridas monótonas (SystemClock.elapsedRealtime()) y registra uno de los cinco resultados definidos en UpdateFetchOutcome:
| Constante de resultado | @IntDef Código |
Propiedades de métricas registradas | Descripción y estado del sistema |
|---|---|---|---|
UpdateFetchOutcome.CACHE_HIT |
1 |
totalDurationMillis, processingDurationMillis, callerUid |
Se entrega de inmediato desde la caché de la memoria del disco o la memoria local en la ruta rápida (se devolvió shouldFetchUpdates() false). lockWaitDurationMillis y fetchDurationMillis son 0. |
UpdateFetchOutcome.COALESCED |
2 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid |
La consulta se puso en cola detrás de otra actualización activa. Cuando se adquirió el bloqueo, los datos estaban actualizados. Se evitó la recuperación duplicada de la red (fetchDurationMillis es 0). |
UpdateFetchOutcome.FETCHED |
3 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis, callerUid |
La sincronización de la red de backend se ejecutó correctamente (se completó fetchUpdates()). Se conservaron registros nuevos en el disco. |
UpdateFetchOutcome.THROTTLED |
4 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid |
El limitador de frecuencia bloqueó la solicitud (se devolvió shouldThrottle() true). Los datos almacenados en caché se devolvieron de forma segura al cliente (fetchDurationMillis es 0). |
UpdateFetchOutcome.FAILED |
5 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis, callerUid |
La verificación de actualización o la solicitud de red generaron una excepción. El firewall de excepciones lo detectó, se activó onFetchFailed(e) y se devolvió la resiliencia almacenada en caché. |
Hook de vinculación avanzada del agente de servicio: getCallerUid()
Cuando un cliente se conecta, UpdateInfoService evalúa automáticamente getCallerUid() en el subproceso inicial de Binder (antes de llamar a Binder.clearCallingIdentity() antes de fetchUpdates()), verifica la propiedad del paquete y pasa el UID del llamador verificado directamente a onClientConnected(), onClientDisconnected() y telemetry.callerUid en onRequestCompleted(telemetry).
En el caso de los componentes <service> estándar de Android, no es necesario que llames ni anules getCallerUid(). El método protected open getCallerUid() (que delega en Binder.getCallingUid() de forma predeterminada) se proporciona como un hook de anulación para las apps host que enrutan la comunicación entre procesos (IPC) de Binder a través de un agente de servicio interno o una arquitectura de proxy, lo que permite que la subclase devuelva el UID del cliente lógico en lugar del UID del agente.
Recursos adicionales
Para obtener más información sobre el estado de seguridad de la publicación, consulta los siguientes recursos:
Documentación
- Información sobre el estado de seguridad del dispositivo
- Boletines de seguridad de Android
- Componentes modulares del sistema
- Parches de seguridad complementarios
- Notas de la versión 1.0.0 de Security State Provider
Referencia de la API
UpdateInfoServiceListenableFutureUpdateInfoServiceUpdateInfoManagerUpdateInfoUpdateInfo.BuilderUpdateCheckTelemetryUpdateFetchOutcomeSecurityPatchState