Gerätesicherheitsstatus veröffentlichen

Wenn Sie ein Original Equipment Manufacturer (OEM) sind oder einen privilegierten OTA-Update-Client (Over-the-Air) verwalten, können Sie sicherheitssensiblen Apps auf dem Gerät Informationen zu ausstehenden Sicherheitsupdates zur Verfügung stellen, damit sie den Sicherheitsstatus des Geräts genau bewerten können. Um robuste Zero-Trust-Richtlinien zu erzwingen, müssen Apps nicht nur den auf dem Gerät installierten Patch-Level (Device Security Patch Level, DSPL) prüfen können, sondern auch, welche Sicherheitsupdates verfügbar und bereit zur Installation sind (Available Security Patch Level, ASPL).

Da Client-Apps ohne Berechtigungen Firmware-Eigenschaften nicht direkt lesen, private Updater-Datenbanken nicht prüfen und keine internen OEM-Backend-Endpunkte abfragen können, bietet die AndroidX Security State Provider-Bibliothek eine standardisierte, sichere IPC-Architektur (Interprocess Communication), die von Update-Clients verwendet wird, um Informationen zu verfügbaren Updates zu teilen. Durch die Implementierung einer UpdateInfoService in Ihrem OTA-Update-Client können Sie ASPL-Metadaten für das System veröffentlichen, ohne proprietäre Backend-Integrationen offenzulegen. Google stellt die Implementierung für Updater für modulare Systemkomponenten (Mainline) für GMS-Geräte bereit. Auf Nicht-GMS-Geräten können auch ASPL-Metadaten für diese modularen Systemkomponenten veröffentlicht werden.

Architekturübersicht

Das folgende Diagramm veranschaulicht, wie die AndroidX Security State Provider-Bibliothek ein standardisiertes, sicheres IPC-Framework zwischen nicht privilegierten Client-Apps und On-Device-Aktualisierungsdiensten einrichtet:

Die AndroidX Security State Provider-Bibliothek stellt ein standardisiertes, sicheres IPC-Framework zwischen nicht privilegierten Client-Apps und On-Device-Update-Diensten her.

Datenbereitstellungsmodelle

Client-Apps fragen die Verfügbarkeit von Updates ab, indem sie queryAllAvailableUpdates oder fetchAvailableSecurityPatchLevel aufrufen. Im Hintergrund werden mit der Clientbibliothek automatisch alle registrierten Dienste erkannt und gebunden, die die UpdateInfoService-Klasse auf dem Gerät von System-Apps mit der Berechtigung READ_PRIVILEGED_PHONE_STATE erweitern.

Wie im vorherigen Diagramm dargestellt, unterstützt die security-state-provider-Bibliothek zwei Datenbereitstellungsmodelle:

Liefermodell Synchronisierungstrigger Antwort des Kunden Empfohlene Anwendungsfälle
Push-Modell (Hintergrundsynchronisierung) Geplante Hintergrund-Worker (WorkManager oder JobScheduler) werden mit Ihrem Backend synchronisiert und schreiben Datensätze in UpdateInfoManager. Ihr Dienst wird immer aus dem lokalen Datenträger-Cache (shouldFetchUpdates() = false) bereitgestellt. Wird sofort aus dem lokalen Cache bereitgestellt. OTA-Updater für OEM-Systeme und Updater für modulare Komponenten, die im Hintergrund synchronisiert werden.
Modell abrufen (On-Demand-Synchronisierung) Eingehende IPC-Anfragen von Clients lösen einen Netzwerkabruf aus, wenn zwischengespeicherte Datensätze veraltet sind (shouldFetchUpdates() = true). Durch die Zusammenfassung von Mutex und die Ratenbegrenzung (shouldThrottle()) wird das Backend vor Spitzen geschützt. Wartet auf den Backend-Abruf, wenn der Cache veraltet ist. Monolithische OEM-OTA-Updater ohne geplante Hintergrundsynchronisierungs-Worker.

Mehrere Update-Anbieter

Auf Android-Produktionsgeräten sind mehrere unabhängige Update-Anbieter gleichzeitig vorhanden. Beispiel: Mainline veröffentlicht die Verfügbarkeit für modulare Komponenten (COMPONENT_SYSTEM_MODULES), während Ihr OEM-OTA-Client Updates für das primäre Betriebssystem-Image (COMPONENT_SYSTEM) veröffentlicht.

Ihr Dienst muss nur Updates für die von ihm verwalteten Komponenten registrieren. Wenn mehrere Anbieter auf einem Gerät Updates für dieselbe Komponente veröffentlichen, werten Client-Apps das höchste verfügbare Patch-Level (mit fetchAvailableSecurityPatchLevel()) aus oder prüfen einzelne UpdateInfo-Datensätze (mit queryAllAvailableUpdates()) für die Unternehmensprüfung. Achten Sie darauf, dass Ihr Dienst immer das kanonische Format für Ihre Komponente veröffentlicht (DateBasedSecurityPatchLevel für COMPONENT_SYSTEM).

Schritt-für-Schritt-Anleitung für das Onboarding Ihres Update-Clients

So integrieren Sie die AndroidX Security State Provider-Bibliothek in Ihren Update-Client und veröffentlichen die Verfügbarkeit von Sicherheitsupdates für Ihr Gerät:

Schritt 1: Abhängigkeiten hinzufügen

Wenn Sie einen Update-Anbieter implementieren möchten, muss Ihr Projekt das Google Maven-Repository enthalten. Fügen Sie dann die security-state-provider-Bibliothek zur Datei build.gradle.kts (Kotlin DSL) oder build.gradle (Groovy DSL) Ihres Moduls hinzu:

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'
}

Schritt 2: Update-Dienst im Manifest deklarieren

Deklarieren Sie Ihren Dienst in der AndroidManifest.xml Ihrer App mit einem <intent-filter>, das mit androidx.security.state.provider.UPDATE_INFO_SERVICE übereinstimmt. Der Dienst muss exportiert (android:exported="true") und als Dienst für einen einzelnen Nutzer (android:singleUser="true") konfiguriert werden, damit die Clientbibliothek prozess- und nutzerübergreifend daran gebunden werden kann, insbesondere für Arbeitsprofile:

<!-- 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>

Wenn Ihr Updater nicht als android.uid.system ausgeführt wird, deklarieren Sie auch die folgenden Berechtigungen in Ihrem Manifest und fügen Sie sie der Zulassungsliste für Berechtigungen mit Sonderrechten hinzu:

  • READ_PRIVILEGED_PHONE_STATE: erforderlich, damit Clients Ihrem Anbieter vertrauen können.
  • INTERACT_ACROSS_USERS: Erforderlich für android:singleUser="true".

Schritt 3: UpdateInfoService implementieren

Wenn Sie Ihren Aktualisierungsstatus veröffentlichen möchten, müssen Sie die Klasse UpdateInfoService implementieren und Aktualisierungsdatensätze erstellen, die dem erwarteten Datenmodell entsprechen.

UpdateInfo-Datenmodellspezifikation

Unabhängig davon, ob Sie das Push- oder das Pull-Modell verwenden, müssen Sie UpdateInfo-Datensätze mit UpdateInfo.Builder gemäß der folgenden Spezifikation erstellen:

Feldname Getter-Methode Datentyp Validierungs- und Formatanforderungen Zweck und Systemsemantik
component getComponent() String (@Component) Kanonische Konstanten in SecurityPatchState: COMPONENT_SYSTEM, COMPONENT_SYSTEM_MODULES oder COMPONENT_KERNEL. Gibt das Software- oder Firmware-Subsystem an, auf das sich dieses Update bezieht.
securityPatchLevel getSecurityPatchLevel() SecurityPatchLevel Muss eine Instanz von DateBasedSecurityPatchLevel (YYYY-MM-DD) oder VersionedSecurityPatchLevel (major.minor.patch) sein oder mit SecurityPatchState.getComponentSecurityPatchLevel() geparst werden. Das Ziel-Sicherheitspatch-Level, das nach der Installation dieses Updates erreicht wird.
publishedDateMillis getPublishedDateMillis() long Millisekunden seit der Unix-Epoche (System.currentTimeMillis()). Muss > 0 sein. Wann das Update für Nutzer verfügbar gemacht wurde, z. B. die OTA-Veröffentlichungszeit. Verwenden Sie nicht die Download- oder Installationszeit der Nutzlast.
lastCheckTimeMillis getLastCheckTimeMillis() long Millisekunden seit der Unix-Epoche. Muss > 0 lauten. Zeitstempel, der angibt, wann Ihr Dienstleister diesen Aktualisierungsdatensatz während der Synchronisierung bestätigt oder erkannt hat.

Wählen Sie aus den folgenden Optionen das Bereitstellungsmodell aus, das zur Architektur Ihres Updater passt:

Option A: Push-Modell (empfohlen)

Wenn Ihr Worker für die Hintergrundsynchronisierung Ihren OTA-Server prüft, müssen Sie bestätigen, dass jedes erkannte Update den aktuellen Patch-Level des Geräts erhöht, und es mit UpdateInfoManager.registerUpdate() beibehalten oder UpdateInfoManager.unregisterUpdate() aufrufen, wenn kein Sicherheitsupdate mit höherem Level aussteht. Rufen Sie UpdateInfoManager.setLastCheckTimeMillis() immer am Ende jeder Synchronisierung auf, auch nach dem Aufrufen von registerUpdate(). Dadurch wird der UpdateInfo-Datensatz für die einzelnen Komponenten beibehalten, aber der globale Zeitstempel für den letzten Check, der an Clients zurückgegeben wird, wird nicht aktualisiert. Sie können dies mit einem WorkManager CoroutineWorker in Kotlin oder Worker in Java implementieren:

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 einem Push-basierten Modell werden Aktualisierungsdatensätze für Hintergrundaufgaben direkt in UpdateInfoManager gespeichert. Wenn Sie das Framework anweisen möchten, immer Datensätze vom lokalen Festplattenspeicher bereitzustellen, überschreiben Sie shouldFetchUpdates(), um false zurückzugeben, indem Sie UpdateInfoService in Kotlin oder ListenableFutureUpdateInfoService in Java erweitern:

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());
    }
}

Option B: Pull-Modell (auf Abruf)

In einer Pull-basierten Architektur verarbeitet Ihr Dienst On-Demand-Aktualisierungsanfragen, die von Client-Apps ausgelöst werden, wenn der lokale Cache veraltet ist.

Um Anfragen für On-Demand-Updates zu verarbeiten, erweitern Sie UpdateInfoService in Kotlin (durch Implementieren der suspendierenden Funktion fetchUpdates()) oder ListenableFutureUpdateInfoService in Java (durch Implementieren von fetchUpdatesAsync(), die eine Guava-ListenableFuture zurückgibt):

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);
    }
}

Schritt 4: Angewendete Updates nach dem Neustart des Geräts löschen

UpdateInfoManager entfernt automatisch veraltete Updates, wenn registerUpdate() aufgerufen wird. Ihr Updater ruft registerUpdate() jedoch erst nach dem nächsten geplanten Server-Synchronisierungszyklus wieder auf, nachdem die Installation eines OTA-Updates abgeschlossen ist. Damit Client-Apps ein bereits installiertes Update nicht sofort nach dem Neustart als noch ausstehend anzeigen, sollten Sie auf ACTION_BOOT_COMPLETED warten und UpdateInfoManager.unregisterUpdate() aufrufen, wenn die Installation eines OTA-Updates abgeschlossen ist, um den Datensatz aus dem lokalen Cache zu löschen. Dadurch wird vermieden, dass ausstehende (nicht installierte) Updates bei jedem normalen Neustart des Geräts bedingungslos gelöscht werden. Da UpdateInfoManager-Schlüssel Datensätze nach Komponente aktualisieren, müssen Sie beim Erstellen des UpdateInfo-Objekts für die Deregistrierung nur die Zielkomponente angeben:

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());

Schritt 5: Integration überprüfen

Führen Sie die folgenden Prüfungen auf einem Android-Gerät oder -Emulator mit der Android Debug Bridge (ADB) aus, um die End-to-End-Integration zu validieren und häufige OEM-Bereitstellungsfehler zu vermeiden:

  1. Prüfen, ob Clients Ihrem Anbieter vertrauen:Client-Apps ignorieren alle Anbieter, die nicht READ_PRIVILEGED_PHONE_STATE haben, auch wenn sie vorinstalliert sind. Prüfen Sie, ob die Berechtigung erteilt wurde:

    adb shell dumpsys package <your_package_name> | grep "READ_PRIVILEGED_PHONE_STATE: granted=true"
    

    Prüfen Sie dann, ob Ihr Dienst auffindbar ist und keine Dienstberechtigung hat. Suchen Sie in der Ausgabe für Ihren Dienst nach exported=true und permission=null:

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

    Wenn ein Kunde Ihren Anbieter immer noch nicht sieht, suchen Sie im Logcat nach Ignoring untrusted update provider aus dem SecurityPatchState-Tag.

  2. Intent-Auflösung sowohl im Nutzer 0 als auch in Arbeitsprofilen prüfen:Prüfen Sie, ob das Android-Betriebssystem PackageManager den exportierten UPDATE_INFO_SERVICE-Intent-Filter sowohl im Hauptnutzer (User 0) als auch in allen aktiven Android Enterprise-Arbeitsprofilen (z. B. User 10) auflöst:

    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. Dienststatus und zwischengespeicherte Datensätze mit dumpsys prüfen:UpdateInfoService überschreibt dump(), um Global Last Check, Should Throttle (den Status des Ratenbegrenzers) und Cached Updates zu melden. Da UpdateInfoService ein gebundener Dienst ist und Clients die Bindung sofort nach der Abfrage aufheben, gibt dumpsys activity service (nothing) aus, wenn kein Client gebunden ist. Starten Sie den Dienst explizit, bevor Sie dumpsys ausführen:

    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>
    

    Beispiel für die Diagnoseausgabe:

    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. Clientbindung auslösen und Telemetrieergebnisse überprüfen:Rufen Sie SecurityPatchState.queryAllAvailableUpdates() über eine Test-App ohne Berechtigungen (die keine Berechtigungen für die Systemsignatur hat) auf. Wenn Sie die Telemetrie-Callbacks implementiert haben, prüfen Sie Folgendes:

    • Prüfen Sie, ob der Client ohne Berechtigungen ohne SecurityException gebunden wird und onClientConnected(packageName, callerUid) auslöst.
    • Für Push-Modell-Anbieter (shouldFetchUpdates() == false): Prüfen Sie, ob onRequestCompleted(telemetry)-Logs UpdateFetchOutcome.CACHE_HIT (1) mit fetchDurationMillis == 0 bei jeder Anfrage.
    • Für Pull-Modell-Anbieter (shouldFetchUpdates() == true): Prüfen Sie, ob onRequestCompleted(telemetry)-Logs UpdateFetchOutcome.FETCHED (3) bei der ersten Anfrage für einen veralteten Cache und CACHE_HIT (1) bei unmittelbar nachfolgenden Anfragen enthalten. Wenn Sie den persistenten Ratenbegrenzer für eine Stunde zwischen Pull-Modell-Testläufen zurücksetzen möchten, führen Sie adb shell pm clear <your_package_name> aus.

Optionale und erweiterte Konfigurationen

Caching-Verfahrensweise und Ratenbegrenzung

Wenn ein Client Updates abfragt, führt UpdateInfoService einen Workflow mit doppelter Sperrung aus, um die Datenaktualität mit der Backend-Serverlast in Einklang zu bringen:

UpdateInfoService führt einen Workflow mit Double-Checked Locking aus, um die Datenaktualität mit der Backend-Serverlast in Einklang zu bringen.

  • Fast Path (shouldFetchUpdates()): Standardmäßig gibt shouldFetchUpdates() nur dann true (veralteter Cache) zurück, wenn der globale lastCheckTimeMillis älter als 1 Stunde (TimeUnit.HOURS.toMillis(1)) ist. Wenn shouldFetchUpdates() false zurückgibt, gibt der Dienst sofort zwischengespeicherte Datensätze mit dem Ergebnis UpdateFetchOutcome.CACHE_HIT zurück, ohne Sperren zu erwerben oder Netzwerk-E/A auszuführen. Sie können shouldFetchUpdates() überschreiben, um diese Caching-Richtlinie anzupassen.
  • Slow Path & Request Coalescing:Wenn shouldFetchUpdates() true zurückgibt, ruft der Dienst einen internen Coroutine-Mutex ab und wertet shouldFetchUpdates() noch einmal aus. Dabei wird UpdateFetchOutcome.COALESCED zurückgegeben, wenn der Cache durch eine gleichzeitige Anfrage bereits aktualisiert wurde, während auf die Sperre gewartet wurde.
  • Persistenter Ratenbegrenzer (shouldThrottle()):shouldThrottle() schützt die Backend-Infrastruktur vor Abfrage-Bursts oder wiederholten Fehlern, indem ein persistentes Mindestintervall von einer Stunde bei App- und Geräteneustarts erzwungen wird. UpdateInfoService zeichnet jeden Versuch auf, bevor fetchUpdates() aufgerufen wird. Wenn fetchUpdates() also eine Ausnahme auslöst (UpdateFetchOutcome.FAILED wird nach dem Aufrufen von onFetchFailed(e) zurückgegeben), werden bei nachfolgenden Anfragen in den nächsten 60 Minuten im Cache gespeicherte Fallback-Daten mit dem Ergebnis UpdateFetchOutcome.THROTTLED zurückgegeben.

Beobachtbarkeit, Telemetrie und Diagnose

UpdateInfoService bietet integrierte Hooks zur Beobachtbarkeit, um die Clientakzeptanz zu verfolgen, die IPC-Latenz zu überwachen und Backend-Fehler zu protokollieren, ohne Low-Level-AIDL-Stubs zu instrumentieren:

Überschreiben Sie diese Callbacks in UpdateInfoService (Kotlin) oder 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);
    }
}

Telemetrieergebnisse und Latenzmesswerte

Mit UpdateCheckTelemetry werden monotone Zeiträume (SystemClock.elapsedRealtime()) gemessen und eines von fünf Ergebnissen zurückgegeben, die in UpdateFetchOutcome definiert sind:

Ergebnis konstant @IntDef Code Protokollierte Messwerteigenschaften Beschreibung und Systemstatus
UpdateFetchOutcome.CACHE_HIT 1 totalDurationMillis, processingDurationMillis, callerUid Wird sofort vom lokalen Festplatten-/Arbeitsspeicher-Cache auf dem Fast Path bereitgestellt (shouldFetchUpdates() zurückgegeben false). lockWaitDurationMillis und fetchDurationMillis sind 0.
UpdateFetchOutcome.COALESCED 2 totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid Die Abfrage wurde in die Warteschlange eingereiht, da eine andere Aktualisierung aktiv war. Nach dem Erhalten der Sperre waren die Daten aktuell. Doppelte Netzwerkabrufe wurden vermieden (fetchDurationMillis ist 0).
UpdateFetchOutcome.FETCHED 3 totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis, callerUid Die Backend-Netzwerksynchronisierung wurde erfolgreich ausgeführt (fetchUpdates() abgeschlossen). Neue Datensätze werden auf der Festplatte gespeichert.
UpdateFetchOutcome.THROTTLED 4 totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid Anfrage wurde vom Ratenbegrenzer blockiert (shouldThrottle() zurückgegeben true). Zwischengespeicherte Daten wurden sicher an den Client zurückgegeben (fetchDurationMillis ist 0).
UpdateFetchOutcome.FAILED 5 totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis, callerUid Bei der Suche nach Updates oder der Netzwerkanfrage ist eine Ausnahme aufgetreten. Von der Ausnahme-Firewall abgefangen, onFetchFailed(e) ausgelöst, zwischengespeicherter Fallback zurückgegeben.

Erweiterter Service Broker-Hook: getCallerUid()

Wenn ein Client eine Verbindung herstellt, wertet UpdateInfoService automatisch getCallerUid() im ursprünglichen Binder-Thread aus (bevor Binder.clearCallingIdentity() vor fetchUpdates() aufgerufen wird), prüft den Paketinhaber und übergibt die überprüfte Anrufer-UID direkt an onClientConnected(), onClientDisconnected() und telemetry.callerUid in onRequestCompleted(telemetry).

Bei Standard-Android-<service>-Komponenten müssen Sie getCallerUid() nicht aufrufen oder überschreiben. Die Methode protected open getCallerUid() (die standardmäßig an Binder.getCallingUid() delegiert) wird als Überschreibungshook für Host-Apps bereitgestellt, die Binder IPC über einen internen Service Broker oder eine Proxy-Architektur weiterleiten. So kann die Unterklasse die logische Client-UID anstelle der UID des Brokers zurückgeben.

Zusätzliche Ressourcen

Weitere Informationen zum Veröffentlichen des Sicherheitsstatus finden Sie in den folgenden Ressourcen:

Dokumentation

API-Referenz