Se sei un produttore di apparecchiature originali (OEM) o gestisci un client di aggiornamento over-the-air (OTA) con privilegi, puoi consentire alle app sensibili alla sicurezza sul dispositivo di visualizzare gli aggiornamenti della sicurezza in attesa, in modo che possano valutare con precisione il livello di sicurezza del dispositivo. Per applicare solide norme zero-trust, le app devono essere in grado di verificare non solo il livello patch installato sul dispositivo (livello patch di sicurezza del dispositivo o DSPL), ma anche quali aggiornamenti della sicurezza sono disponibili e pronti per l'installazione (livello patch di sicurezza disponibile o ASPL).
Poiché le app client senza privilegi non possono leggere direttamente le proprietà del firmware, ispezionare i database privati del programma di aggiornamento o eseguire query sugli endpoint di backend OEM interni, la libreria AndroidX Security State Provider fornisce un'architettura di comunicazione tra processi (IPC) standardizzata e sicura che i client di aggiornamento utilizzano per condividere informazioni sugli aggiornamenti disponibili. Implementando un
UpdateInfoService nel client di aggiornamento OTA, puoi pubblicare metadati ASPL
per il sistema senza esporre integrazioni di backend proprietarie. Sebbene
Google fornisca l'implementazione degli aggiornamenti dei componenti di sistema modulari (Mainline)
per i dispositivi GMS, anche i dispositivi non GMS possono pubblicare metadati ASPL per
questi componenti di sistema modulari.
Panoramica dell'architettura
Il seguente diagramma illustra come la libreria AndroidX Security State Provider stabilisce un framework IPC standardizzato e sicuro tra app client senza privilegi e servizi di aggiornamento sul dispositivo:

Modelli di invio dei dati
Le app client eseguono query sulla disponibilità degli aggiornamenti chiamando
queryAllAvailableUpdates o
fetchAvailableSecurityPatchLevel. A livello interno, la libreria
client rileva e associa automaticamente tutti i servizi registrati che
estendono la classe UpdateInfoService sul dispositivo dalle app di sistema che
dispongono dell'autorizzazione READ_PRIVILEGED_PHONE_STATE.
Come illustrato nel diagramma precedente, la libreria security-state-provider
supporta due modelli di distribuzione dei dati:
| Modello di pubblicazione | Attivatore di sincronizzazione | Risposta del cliente | Casi d'uso consigliati |
|---|---|---|---|
| Modello push (sincronizzazione in background) | I worker in background pianificati (WorkManager o JobScheduler) si sincronizzano con il backend e scrivono record in UpdateInfoManager. Il tuo servizio viene sempre erogato dalla cache del disco locale (shouldFetchUpdates() = false). |
Pubblicati immediatamente dalla cache locale. | Programmi di aggiornamento OTA del sistema OEM e programmi di aggiornamento dei componenti modulari sincronizzati in background. |
| Modello pull (sincronizzazione on demand) | Le query IPC client in entrata attivano un recupero di rete quando i record memorizzati nella cache non sono aggiornati (shouldFetchUpdates() = true). Il raggruppamento e la limitazione della frequenza dei mutex (shouldThrottle()) proteggono il backend dai picchi. |
Attende il recupero dal backend quando la cache è obsoleta. | Aggiornamenti OTA OEM monolitici senza worker di sincronizzazione in background pianificati. |
Più fornitori di aggiornamenti
Sui dispositivi Android di produzione, coesistono più fornitori di aggiornamenti indipendenti
contemporaneamente. Ad esempio, Mainline pubblica la disponibilità di componenti modulari (COMPONENT_SYSTEM_MODULES), mentre il client OTA OEM pubblica gli aggiornamenti per l'immagine sistema operativo principale (COMPONENT_SYSTEM).
Il servizio deve registrare solo gli aggiornamenti per i componenti specifici che gestisce. Se più fornitori su un dispositivo pubblicano aggiornamenti per lo stesso componente, le app client valutano il livello di patch più alto disponibile (utilizzando fetchAvailableSecurityPatchLevel()) o esaminano i singoli record UpdateInfo (utilizzando queryAllAvailableUpdates()) per l'audit aziendale. Assicurati che il servizio pubblichi sempre il formato canonico per il componente (DateBasedSecurityPatchLevel per COMPONENT_SYSTEM).
Guida passo passo per l'onboarding del client di aggiornamento
Segui questi passaggi per integrare la libreria AndroidX Security State Provider nel tuo client di aggiornamento e iniziare a pubblicare la disponibilità dell'aggiornamento della sicurezza del tuo dispositivo.
Passaggio 1: aggiungi le dipendenze
Per implementare un provider di aggiornamenti, assicurati che il tuo progetto includa il repository Maven di Google, quindi aggiungi la libreria security-state-provider al file build.gradle.kts (Kotlin DSL) o build.gradle (Groovy DSL) del tuo modulo:
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")
}
Alla moda
// 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'
}
Passaggio 2: dichiara il servizio di aggiornamento nel manifest
Dichiara il servizio nel AndroidManifest.xml della tua app con un
<intent-filter> corrispondente a
androidx.security.state.provider.UPDATE_INFO_SERVICE. Il servizio deve essere
esportato (android:exported="true") e configurato come servizio per un singolo utente
(android:singleUser="true") in modo che la libreria client possa associarsi a esso oltre i limiti di processo e utente, in particolare per i profili di lavoro:
<!-- 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>
Se l'updater non viene eseguito come android.uid.system, dichiara anche le seguenti autorizzazioni nel manifest e assicurati di aggiungerle all'allowlist delle autorizzazioni privilegiate:
READ_PRIVILEGED_PHONE_STATE: obbligatorio per consentire ai client di considerare attendibile il tuo fornitore.INTERACT_ACROSS_USERS: obbligatorio perandroid:singleUser="true".
Passaggio 3: implementa UpdateInfoService
Per pubblicare lo stato dell'aggiornamento, devi implementare la classe UpdateInfoService e creare record di aggiornamento che corrispondano al modello di dati previsto.
Specifiche del modello di dati UpdateInfo
Indipendentemente dal modello Push o Pull scelto, crea record UpdateInfo
utilizzando UpdateInfo.Builder in base alle seguenti
specifiche:
| Nome campo | Metodo getter | Tipo di dati | Requisiti di convalida e formato | Scopo e semantica del sistema |
|---|---|---|---|---|
component |
getComponent() |
String (@Component) |
Costanti canoniche in SecurityPatchState: COMPONENT_SYSTEM, COMPONENT_SYSTEM_MODULES o COMPONENT_KERNEL. |
Identifica il sottosistema software o firmware a cui è destinato questo aggiornamento. |
securityPatchLevel |
getSecurityPatchLevel() |
SecurityPatchLevel |
Deve essere un'istanza di DateBasedSecurityPatchLevel (YYYY-MM-DD) o VersionedSecurityPatchLevel (major.minor.patch) oppure analizzato utilizzando SecurityPatchState.getComponentSecurityPatchLevel(). |
Il livello patch di sicurezza di destinazione che verrà raggiunto una volta installato questo aggiornamento. |
publishedDateMillis |
getPublishedDateMillis() |
long |
Millisecondi dall'epoca di Unix (System.currentTimeMillis()). Deve essere > 0. |
Quando l'aggiornamento è stato reso disponibile agli utenti, ad esempio l'ora di rilascio OTA. Non utilizzare il tempo di download o installazione del payload. |
lastCheckTimeMillis |
getLastCheckTimeMillis() |
long |
Millisecondi dall'epoca di Unix. Deve essere > 0. |
Timestamp in cui il tuo fornitore ha verificato o scoperto questo record di aggiornamento durante la sincronizzazione. |
Scegli il modello di distribuzione adatto all'architettura del tuo programma di aggiornamento tra le seguenti opzioni:
Opzione A: modello push (consigliato)
Quando il worker di sincronizzazione in background controlla il server OTA, verifica che l'aggiornamento rilevato aumenti il livello di patch corrente del dispositivo e lo renda persistente utilizzando UpdateInfoManager.registerUpdate() o chiama UpdateInfoManager.unregisterUpdate() se non è in attesa alcun aggiornamento della sicurezza che aumenti il livello di patch. Chiama sempre UpdateInfoManager.setLastCheckTimeMillis() alla fine di ogni sincronizzazione (anche dopo aver chiamato registerUpdate(), che mantiene il record UpdateInfo per componente, ma non aggiorna il timestamp dell'ultimo controllo globale restituito ai client). Puoi implementare questa funzionalità con un
WorkManager CoroutineWorker in Kotlin o Worker in 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();
}
}
In un modello basato sul push, le attività in background rendono persistenti i record di aggiornamento direttamente in
UpdateInfoManager. Per indicare al framework di pubblicare sempre i record dall'archiviazione su disco locale, esegui l'override di shouldFetchUpdates() per restituire false estendendo UpdateInfoService in Kotlin o ListenableFutureUpdateInfoService in 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());
}
}
Opzione B: modello pull (on demand)
In un'architettura basata sul pull, il servizio gestisce le richieste di aggiornamento on demand attivate dalle app client quando la cache locale non è aggiornata.
Per gestire le query di aggiornamento on demand, estendi UpdateInfoService in Kotlin
(implementando la funzione di sospensione fetchUpdates()) o
ListenableFutureUpdateInfoService in Java (implementando
fetchUpdatesAsync() che restituisce un ListenableFuture 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);
}
}
Passaggio 4: cancella gli aggiornamenti applicati dopo il riavvio del dispositivo
UpdateInfoManager elimina automaticamente gli aggiornamenti obsoleti ogni volta che viene richiamato registerUpdate(), ma il programma di aggiornamento non chiamerà di nuovo registerUpdate() dopo che l'installazione di un aggiornamento OTA è terminata fino al successivo ciclo di sincronizzazione del server pianificato. Per impedire alle app client di visualizzare un
aggiornamento già installato come ancora in attesa immediatamente dopo il riavvio, ascolta
ACTION_BOOT_COMPLETED e chiama
UpdateInfoManager.unregisterUpdate() quando l'installazione di un aggiornamento OTA è terminata
per cancellare il record dalla cache locale. In questo modo, solo al termine dell'installazione di un aggiornamento vengono eliminati in modo incondizionato gli aggiornamenti in attesa
(disinstallati) a ogni riavvio normale del dispositivo. Poiché le chiavi
UpdateInfoManager aggiornano i record per
componente, devi specificare solo il componente di destinazione quando crei l'oggetto
UpdateInfo per l'annullamento della registrazione:
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());
Passaggio 5: verifica l'integrazione
Esegui i seguenti controlli su un dispositivo Android o un emulatore utilizzando Android Debug Bridge (ADB) per convalidare l'integrazione end-to-end ed evitare le insidie comuni del deployment OEM:
Verifica che i client si fidino del tuo fornitore:le app client ignorano qualsiasi fornitore che non disponga di
READ_PRIVILEGED_PHONE_STATE, anche se è preinstallato. Verifica che l'autorizzazione sia concessa:adb shell dumpsys package <your_package_name> | grep "READ_PRIVILEGED_PHONE_STATE: granted=true"Poi verifica che il servizio sia rilevabile e non disponga di autorizzazioni di servizio. Nell'output del servizio, controlla
exported=trueepermission=null:adb shell pm query-services --user 0 -a androidx.security.state.provider.UPDATE_INFO_SERVICESe un client ancora non vede il tuo fornitore, controlla logcat per
Ignoring untrusted update providerdal tagSecurityPatchState.Verifica la risoluzione degli intent sia nel profilo utente 0 che nel profilo di lavoro: verifica che il sistema operativo Android
PackageManagerrisolva il filtroUPDATE_INFO_SERVICEdegli intent esportati sia nell'utente principale (User 0) sia in qualsiasi profilo di lavoro Android Enterprise attivo (ad esempioUser 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 lo stato del servizio e i record memorizzati nella cache utilizzando
dumpsys:UpdateInfoServiceesegue l'override didump()per segnalareGlobal Last Check,Should Throttle(lo stato del limite di frequenza) eCached Updates. PoichéUpdateInfoServiceè un servizio associato e i client si svincolano immediatamente dopo la query,dumpsys activity servicerestituisce(nothing)quando non è associato alcun client. Avvia il servizio in modo esplicito prima di eseguiredumpsys: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>Output di diagnostica di esempio:
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 2026Attiva l'associazione client e verifica i risultati della telemetria:da un'app di test senza privilegi (che non dispone delle autorizzazioni di firma del sistema), richiama
SecurityPatchState.queryAllAvailableUpdates(). Se hai implementato i callback di telemetria, controlla quanto segue:- Verifica che il client senza privilegi si associ senza
SecurityExceptione attivionClientConnected(packageName, callerUid). - Per i fornitori di modelli push (
shouldFetchUpdates() == false): verifica cheonRequestCompleted(telemetry)i logUpdateFetchOutcome.CACHE_HIT(1) confetchDurationMillis == 0siano presenti in ogni query. - Per i provider con modello pull (
shouldFetchUpdates() == true): verifica che i logonRequestCompleted(telemetry)UpdateFetchOutcome.FETCHED(3) nella query iniziale della cache obsoleta, seguiti daCACHE_HIT(1) nelle query successive immediate. (Per reimpostare il limite di frequenza persistente di 1 ora tra le esecuzioni di test Pull-model, eseguiadb shell pm clear <your_package_name>.)
- Verifica che il client senza privilegi si associ senza
Configurazioni facoltative e avanzate
Policy di memorizzazione nella cache e limitazione della frequenza
Quando un client esegue query sugli aggiornamenti, UpdateInfoService esegue un flusso di lavoro di blocco a doppio controllo per bilanciare l'aggiornamento dei dati rispetto al carico del server di backend:

- Fast Path (
shouldFetchUpdates()): per impostazione predefinita,shouldFetchUpdates()restituiscetrue(che indica una cache obsoleta) solo quandolastCheckTimeMillisglobale è più vecchio di 1 ora (TimeUnit.HOURS.toMillis(1)). QuandoshouldFetchUpdates()restituiscefalse, il servizio restituisce immediatamente i record memorizzati nella cache con il risultatoUpdateFetchOutcome.CACHE_HITsenza acquisire blocchi o eseguire I/O di rete. Puoi eseguire l'override dishouldFetchUpdates()per personalizzare questa norma di memorizzazione nella cache. - Slow Path e unione delle richieste: quando
shouldFetchUpdates()restituiscetrue, il servizio acquisisce un mutex di coroutine interno e rivalutashouldFetchUpdates()(restituendoUpdateFetchOutcome.COALESCEDse una richiesta in parallelo ha già aggiornato la cache durante l'attesa del blocco). - Limitatore di frequenza persistente (
shouldThrottle()): per proteggere l'infrastruttura di backend da picchi di query o errori ripetuti,shouldThrottle()impone un intervallo minimo persistente di 1 ora tra i riavvii di app e dispositivi.UpdateInfoServiceregistra ogni tentativo prima di richiamarefetchUpdates(), quindi sefetchUpdates()genera un'eccezione (restituendoUpdateFetchOutcome.FAILEDdopo aver richiamatoonFetchFailed(e)), le query successive durante i 60 minuti successivi restituiscono normalmente i dati di riserva memorizzati nella cache con il risultatoUpdateFetchOutcome.THROTTLED.
Osservabilità, telemetria e diagnostica
UpdateInfoService fornisce hook di osservabilità integrati per monitorare l'adozione
del client, monitorare la latenza IPC e registrare gli errori di backend senza instrumentare
gli stub AIDL di basso livello:
onRequestCompleted(telemetry): viene richiamato al termine di ogni controllo degli aggiornamenti con un riepilogo diUpdateCheckTelemetry.onClientConnected(packageName, callerUid): viene richiamato quando un client verificato apre una sessione.onClientDisconnected(packageName, callerUid): viene richiamato quando un client annulla l'associazione o il relativo processo termina.onFetchFailed(e): viene richiamato se si verifica un'eccezione durantefetchUpdates(), prima che il servizio restituisca i dati di riserva memorizzati nella cache.
Esegui l'override di questi callback in 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);
}
}
Metriche di latenza e risultati della telemetria
UpdateCheckTelemetry misura le durate trascorse monotoniche
(SystemClock.elapsedRealtime()) e riporta uno dei cinque risultati definiti
in UpdateFetchOutcome:
| Costante del risultato | @IntDef Codice |
Proprietà metrica registrate | Descrizione e stato del sistema |
|---|---|---|---|
UpdateFetchOutcome.CACHE_HIT |
1 |
totalDurationMillis, processingDurationMillis e callerUid |
Servito immediatamente dalla cache del disco locale/della memoria sul percorso rapido (shouldFetchUpdates() restituito false). lockWaitDurationMillis e fetchDurationMillis sono 0. |
UpdateFetchOutcome.COALESCED |
2 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid |
Query in coda dietro un altro aggiornamento attivo; dopo aver acquisito il blocco, i dati erano aggiornati. È stato evitato il recupero di una rete duplicata (fetchDurationMillis è 0). |
UpdateFetchOutcome.FETCHED |
3 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis, callerUid |
Sincronizzazione della rete di backend eseguita correttamente (fetchUpdates() completato). Nuovi record salvati su disco. |
UpdateFetchOutcome.THROTTLED |
4 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid |
Richiesta bloccata dal limitatore di velocità (shouldThrottle() ha restituito true). I dati memorizzati nella cache sono stati restituiti in modo sicuro al client (fetchDurationMillis è 0). |
UpdateFetchOutcome.FAILED |
5 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis, callerUid |
La verifica degli aggiornamenti o la richiesta di rete ha generato un'eccezione. Rilevato dal firewall di eccezione, attivato onFetchFailed(e), restituito il fallback memorizzato nella cache. |
Hook avanzato del service broker: getCallerUid()
Quando un client si connette, UpdateInfoService valuta automaticamente
getCallerUid() nel thread Binder iniziale (prima di chiamare
Binder.clearCallingIdentity() prima di fetchUpdates()), verifica
la proprietà del pacchetto e passa l'UID del chiamante verificato direttamente a
onClientConnected(), onClientDisconnected() e telemetry.callerUid in
onRequestCompleted(telemetry).
Per i componenti Android <service> standard, non è necessario chiamare o eseguire l'override di
getCallerUid(). Il metodo protected open getCallerUid() (che delega a
Binder.getCallingUid() per impostazione predefinita) viene fornito come hook di override per
le app host che instradano Binder IPC tramite un service broker interno o
un'architettura proxy, consentendo alla sottoclasse di restituire l'UID client logico
anziché l'UID del broker.
Risorse aggiuntive
Per saperne di più sulla pubblicazione dello stato di sicurezza, consulta le seguenti risorse:
Documentazione
- Informazioni sullo stato di sicurezza del dispositivo
- Bollettini sulla sicurezza di Android
- Componenti del sistema modulare
- Patch di sicurezza supplementari
- Note di rilascio di Security State Provider 1.0.0
Riferimento API
UpdateInfoServiceListenableFutureUpdateInfoServiceUpdateInfoManagerUpdateInfoUpdateInfo.BuilderUpdateCheckTelemetryUpdateFetchOutcomeSecurityPatchState