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:

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ürandroid: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:
Prüfen, ob Clients Ihrem Anbieter vertrauen:Client-Apps ignorieren alle Anbieter, die nicht
READ_PRIVILEGED_PHONE_STATEhaben, 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=trueundpermission=null:adb shell pm query-services --user 0 -a androidx.security.state.provider.UPDATE_INFO_SERVICEWenn ein Kunde Ihren Anbieter immer noch nicht sieht, suchen Sie im Logcat nach
Ignoring untrusted update provideraus demSecurityPatchState-Tag.Intent-Auflösung sowohl im Nutzer 0 als auch in Arbeitsprofilen prüfen:Prüfen Sie, ob das Android-Betriebssystem
PackageManagerden exportiertenUPDATE_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_SERVICEDienststatus und zwischengespeicherte Datensätze mit
dumpsysprüfen:UpdateInfoServiceüberschreibtdump(), umGlobal Last Check,Should Throttle(den Status des Ratenbegrenzers) undCached Updateszu melden. DaUpdateInfoServiceein gebundener Dienst ist und Clients die Bindung sofort nach der Abfrage aufheben, gibtdumpsys activity service(nothing)aus, wenn kein Client gebunden ist. Starten Sie den Dienst explizit, bevor Siedumpsysausfü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 2026Clientbindung 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
SecurityExceptiongebunden wird undonClientConnected(packageName, callerUid)auslöst. - Für Push-Modell-Anbieter (
shouldFetchUpdates() == false): Prüfen Sie, obonRequestCompleted(telemetry)-LogsUpdateFetchOutcome.CACHE_HIT(1) mitfetchDurationMillis == 0bei jeder Anfrage. - Für Pull-Modell-Anbieter (
shouldFetchUpdates() == true): Prüfen Sie, obonRequestCompleted(telemetry)-LogsUpdateFetchOutcome.FETCHED(3) bei der ersten Anfrage für einen veralteten Cache undCACHE_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 Sieadb shell pm clear <your_package_name>aus.
- Prüfen Sie, ob der Client ohne Berechtigungen ohne
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:

- Fast Path (
shouldFetchUpdates()): Standardmäßig gibtshouldFetchUpdates()nur danntrue(veralteter Cache) zurück, wenn der globalelastCheckTimeMillisälter als 1 Stunde (TimeUnit.HOURS.toMillis(1)) ist. WennshouldFetchUpdates()falsezurückgibt, gibt der Dienst sofort zwischengespeicherte Datensätze mit dem ErgebnisUpdateFetchOutcome.CACHE_HITzurück, ohne Sperren zu erwerben oder Netzwerk-E/A auszuführen. Sie könnenshouldFetchUpdates()überschreiben, um diese Caching-Richtlinie anzupassen. - Slow Path & Request Coalescing:Wenn
shouldFetchUpdates()truezurückgibt, ruft der Dienst einen internen Coroutine-Mutex ab und wertetshouldFetchUpdates()noch einmal aus. Dabei wirdUpdateFetchOutcome.COALESCEDzurü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.UpdateInfoServicezeichnet jeden Versuch auf, bevorfetchUpdates()aufgerufen wird. WennfetchUpdates()also eine Ausnahme auslöst (UpdateFetchOutcome.FAILEDwird nach dem Aufrufen vononFetchFailed(e)zurückgegeben), werden bei nachfolgenden Anfragen in den nächsten 60 Minuten im Cache gespeicherte Fallback-Daten mit dem ErgebnisUpdateFetchOutcome.THROTTLEDzurü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:
onRequestCompleted(telemetry):Wird nach Abschluss jeder Updateprüfung mit einerUpdateCheckTelemetry-Zusammenfassung aufgerufen.onClientConnected(packageName, callerUid):Wird aufgerufen, wenn ein bestätigter Client eine Sitzung öffnet.onClientDisconnected(packageName, callerUid): Wird aufgerufen, wenn ein Client die Bindung aufhebt oder sein Prozess beendet wird.onFetchFailed(e):Wird aufgerufen, wenn währendfetchUpdates()eine Ausnahme auftritt, bevor der Dienst zwischengespeicherte Fallback-Daten zurückgibt.
Ü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
- Gerätesicherheitsstatus
- Android-Sicherheitsbulletins
- Modulare Systemkomponenten
- Zusätzliche Sicherheitspatches
- Versionshinweise zu Security State Provider 1.0.0
API-Referenz
UpdateInfoServiceListenableFutureUpdateInfoServiceUpdateInfoManagerUpdateInfoUpdateInfo.BuilderUpdateCheckTelemetryUpdateFetchOutcomeSecurityPatchState