Pubblica lo stato di sicurezza del dispositivo

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:

La libreria AndroidX Security State Provider stabilisce un framework IPC standardizzato e sicuro tra le app client senza privilegi e i 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 per android: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:

  1. 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=true e permission=null:

    adb shell pm query-services --user 0 -a androidx.security.state.provider.UPDATE_INFO_SERVICE
    

    Se un client ancora non vede il tuo fornitore, controlla logcat per Ignoring untrusted update provider dal tag SecurityPatchState.

  2. Verifica la risoluzione degli intent sia nel profilo utente 0 che nel profilo di lavoro: verifica che il sistema operativo Android PackageManager risolva il filtro UPDATE_INFO_SERVICE degli intent esportati sia nell'utente principale (User 0) sia in qualsiasi profilo di lavoro Android Enterprise attivo (ad esempio User 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_SERVICE
    
  3. Verifica lo stato del servizio e i record memorizzati nella cache utilizzando dumpsys: UpdateInfoService esegue l'override di dump() per segnalare Global Last Check, Should Throttle (lo stato del limite di frequenza) e Cached Updates. Poiché UpdateInfoService è un servizio associato e i client si svincolano immediatamente dopo la query, dumpsys activity service restituisce (nothing) quando non è associato alcun client. Avvia il servizio in modo esplicito prima di eseguire dumpsys:

    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 2026
    
  4. Attiva 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 SecurityException e attivi onClientConnected(packageName, callerUid).
    • Per i fornitori di modelli push (shouldFetchUpdates() == false): verifica cheonRequestCompleted(telemetry) i log UpdateFetchOutcome.CACHE_HIT (1) con fetchDurationMillis == 0 siano presenti in ogni query.
    • Per i provider con modello pull (shouldFetchUpdates() == true): verifica che i log onRequestCompleted(telemetry) UpdateFetchOutcome.FETCHED (3) nella query iniziale della cache obsoleta, seguiti da CACHE_HIT (1) nelle query successive immediate. (Per reimpostare il limite di frequenza persistente di 1 ora tra le esecuzioni di test Pull-model, esegui adb shell pm clear <your_package_name>.)

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:

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() restituisce true (che indica una cache obsoleta) solo quando lastCheckTimeMillis globale è più vecchio di 1 ora (TimeUnit.HOURS.toMillis(1)). Quando shouldFetchUpdates() restituisce false, il servizio restituisce immediatamente i record memorizzati nella cache con il risultato UpdateFetchOutcome.CACHE_HIT senza acquisire blocchi o eseguire I/O di rete. Puoi eseguire l'override di shouldFetchUpdates() per personalizzare questa norma di memorizzazione nella cache.
  • Slow Path e unione delle richieste: quando shouldFetchUpdates() restituisce true, il servizio acquisisce un mutex di coroutine interno e rivaluta shouldFetchUpdates() (restituendo UpdateFetchOutcome.COALESCED se 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. UpdateInfoService registra ogni tentativo prima di richiamare fetchUpdates(), quindi se fetchUpdates() genera un'eccezione (restituendo UpdateFetchOutcome.FAILED dopo aver richiamato onFetchFailed(e)), le query successive durante i 60 minuti successivi restituiscono normalmente i dati di riserva memorizzati nella cache con il risultato UpdateFetchOutcome.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:

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

Riferimento API