Jeśli jesteś producentem oryginalnego sprzętu (OEM) lub utrzymujesz uprzywilejowanego klienta aktualizacji bezprzewodowych (OTA), możesz udostępnić aplikacjom na urządzeniu, które są wrażliwe na kwestie bezpieczeństwa, informacje o oczekujących aktualizacjach zabezpieczeń, aby mogły one dokładnie ocenić poziom bezpieczeństwa urządzenia. Aby egzekwować solidne zasady zerowego zaufania, aplikacje muszą mieć możliwość weryfikowania nie tylko poziomu aktualizacji zainstalowanego na urządzeniu (poziom aktualizacji zabezpieczeń urządzenia, DSPL), ale także tego, jakie aktualizacje zabezpieczeń są dostępne i gotowe do zainstalowania (dostępny poziom aktualizacji zabezpieczeń, ASPL).
Nieuprawnione aplikacje klienckie nie mogą bezpośrednio odczytywać właściwości oprogramowania sprzętowego, sprawdzać prywatnych baz danych modułu aktualizacji ani wysyłać zapytań do wewnętrznych punktów końcowych backendu OEM. Dlatego biblioteka AndroidX Security State Provider udostępnia standardową, bezpieczną architekturę komunikacji międzyprocesowej (IPC), której klienci aktualizacji używają do udostępniania informacji o dostępnych aktualizacjach. Wdrażając w klientach aktualizacji OTA UpdateInfoService, możesz publikować metadane ASPL
dla systemu bez ujawniania zastrzeżonych integracji backendu. Google udostępnia implementację modułowych komponentów systemu (Mainline) dla urządzeń z GMS, ale urządzenia bez GMS mogą też publikować metadane ASPL dla tych modułowych komponentów systemu.
Omówienie architektury
Poniższy diagram ilustruje, jak biblioteka AndroidX Security State Provider tworzy standardową, bezpieczną strukturę IPC między aplikacjami klienckimi bez uprawnień a usługami aktualizacji na urządzeniu:

Modele dostarczania danych
Aplikacje klienckie sprawdzają dostępność aktualizacji, wywołując funkcję
queryAllAvailableUpdates lub
fetchAvailableSecurityPatchLevel. Biblioteka klienta automatycznie wykrywa i wiąże wszystkie zarejestrowane usługi, które rozszerzają klasę UpdateInfoService na urządzeniu, z aplikacji systemowych, które mają uprawnienie READ_PRIVILEGED_PHONE_STATE.
Jak widać na powyższym diagramie, biblioteka security-state-provider obsługuje 2 modele dostarczania danych:
| Model dostawy | Reguła synchronizacji | Odpowiedź klienta | Zalecane przypadki użycia |
|---|---|---|---|
| Model push (synchronizacja w tle) | Zaplanowane procesy działające w tle (WorkManager lub JobScheduler) synchronizują się z backendem i zapisują rekordy w UpdateInfoManager. Usługa zawsze obsługuje żądania z pamięci podręcznej na dysku lokalnym (shouldFetchUpdates() = false). |
są od razu pobierane z lokalnej pamięci podręcznej. | Aktualizatory OTA systemu OEM i aktualizatory komponentów modułowych synchronizowane w tle. |
| Pobieranie modelu (synchronizacja na żądanie) | Przychodzące zapytania IPC klienta wywołują pobieranie z sieci, gdy zapisane w pamięci podręcznej rekordy są nieaktualne (shouldFetchUpdates() = true). Łączenie mutexów i ograniczanie częstotliwości (shouldThrottle()) chronią backend przed nagłymi wzrostami obciążenia. |
Oczekiwanie na pobranie z backendu, gdy pamięć podręczna jest nieaktualna. | Monolityczne programy do aktualizacji OTA OEM bez zaplanowanych procesów synchronizacji w tle. |
Wielu dostawców aktualizacji
Na urządzeniach z Androidem w wersji produkcyjnej jednocześnie działa wielu niezależnych dostawców aktualizacji. Na przykład Mainline publikuje informacje o dostępności modułowych komponentów (COMPONENT_SYSTEM_MODULES), a klient OTA OEM publikuje aktualizacje głównego obrazu systemu operacyjnego (COMPONENT_SYSTEM).
Usługa musi rejestrować aktualizacje tylko w przypadku komponentów, którymi zarządza. Jeśli wielu dostawców na urządzeniu publikuje aktualizacje tego samego komponentu, aplikacje klienckie oceniają najwyższy dostępny poziom poprawki (za pomocą fetchAvailableSecurityPatchLevel()) lub sprawdzają poszczególne rekordy UpdateInfo (za pomocą queryAllAvailableUpdates()) na potrzeby audytu w firmie. Upewnij się, że usługa zawsze publikuje kanoniczny format komponentu (DateBasedSecurityPatchLevel w przypadku COMPONENT_SYSTEM).
Szczegółowy przewodnik dotyczący rejestracji klienta aktualizacji
Aby zintegrować bibliotekę AndroidX Security State Provider z klientem aktualizacji i zacząć publikować informacje o dostępności aktualizacji zabezpieczeń urządzenia, wykonaj te czynności.
Krok 1. Dodaj zależności
Aby wdrożyć dostawcę aktualizacji, upewnij się, że Twój projekt zawiera repozytorium Google Maven, a następnie dodaj bibliotekę security-state-provider do pliku build.gradle.kts (Kotlin DSL) lub build.gradle (Groovy DSL) modułu:
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")
}
Dynamiczny
// 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'
}
Krok 2. Zadeklaruj usługę aktualizacji w pliku manifestu
Zadeklaruj usługę w pliku AndroidManifest.xml aplikacji za pomocą elementu <intent-filter> pasującego do elementu androidx.security.state.provider.UPDATE_INFO_SERVICE. Usługa musi być wyeksportowana (android:exported="true") i skonfigurowana jako usługa dla jednego użytkownika (android:singleUser="true"), aby biblioteka klienta mogła się z nią powiązać w ramach różnych procesów i użytkowników, zwłaszcza w przypadku profili służbowych:
<!-- 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>
Jeśli program do aktualizacji nie działa jako android.uid.system, zadeklaruj w manifeście te uprawnienia i dodaj je do listy dozwolonych uprawnień:
READ_PRIVILEGED_PHONE_STATE: wymagane, aby klienci ufali Twojemu dostawcy.INTERACT_ACROSS_USERS: wymagane w przypadkuandroid:singleUser="true".
Krok 3. Wdróż UpdateInfoService
Aby opublikować stan aktualizacji, musisz zaimplementować klasę UpdateInfoService i utworzyć rekordy aktualizacji zgodne z oczekiwanym modelem danych.
Specyfikacja modelu danych UpdateInfo
Niezależnie od tego, czy wybierzesz model Push czy Pull, utwórz rekordy UpdateInfo za pomocą UpdateInfo.Builder zgodnie z tą specyfikacją:
| Nazwa pola | Metoda pobierająca | Typ danych | Wymagania dotyczące weryfikacji i formatu | Cel i semantyka systemu |
|---|---|---|---|---|
component |
getComponent() |
String (@Component) |
Stałe kanoniczne w SecurityPatchState: COMPONENT_SYSTEM, COMPONENT_SYSTEM_MODULES lub COMPONENT_KERNEL. |
Określa podsystem oprogramowania lub oprogramowania układowego, do którego jest przeznaczona ta aktualizacja. |
securityPatchLevel |
getSecurityPatchLevel() |
SecurityPatchLevel |
Musi to być instancja elementu DateBasedSecurityPatchLevel (YYYY-MM-DD) lub VersionedSecurityPatchLevel (major.minor.patch) albo musi być analizowana za pomocą funkcji SecurityPatchState.getComponentSecurityPatchLevel(). |
Docelowy poziom aktualizacji zabezpieczeń, który zostanie osiągnięty po zainstalowaniu tej aktualizacji. |
publishedDateMillis |
getPublishedDateMillis() |
long |
Milisekundy od początku epoki uniksowej (System.currentTimeMillis()). Musi być > 0. |
Kiedy aktualizacja została udostępniona użytkownikom, np. czas wydania OTA. Nie używaj czasu pobierania ani instalacji pakietu. |
lastCheckTimeMillis |
getLastCheckTimeMillis() |
long |
Milisekundy od początku epoki uniksowej. Musi to być > 0. |
Sygnatura czasowa określająca, kiedy dostawca zweryfikował lub wykrył ten rekord aktualizacji podczas synchronizacji. |
Wybierz model dostawy, który pasuje do architektury narzędzia do aktualizacji, spośród tych opcji:
Opcja A. Model push (zalecany)
Gdy proces roboczy synchronizacji w tle sprawdzi serwer OTA, sprawdź, czy wykryta aktualizacja zwiększa bieżący poziom poprawek urządzenia, i zapisz ją za pomocą funkcji UpdateInfoManager.registerUpdate() lub wywołaj funkcję UpdateInfoManager.unregisterUpdate(), jeśli nie ma oczekującej aktualizacji zabezpieczeń. Zawsze wywołuj UpdateInfoManager.setLastCheckTimeMillis() na końcu każdej synchronizacji (nawet po wywołaniu registerUpdate(), które zachowuje rekord UpdateInfo dla poszczególnych komponentów, ale nie aktualizuje globalnego znacznika czasu ostatniego sprawdzenia zwracanego klientom). Możesz to zrobić za pomocą WorkManager CoroutineWorker w Kotlinie lub Worker w Javie:
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();
}
}
W modelu opartym na wysyłaniu powiadomień zadania w tle zapisują rekordy aktualizacji bezpośrednio w UpdateInfoManager. Aby poinstruować platformę, aby zawsze udostępniała rekordy z lokalnej pamięci dyskowej, zastąp metodę shouldFetchUpdates(), aby zwracała wartość false, rozszerzając klasę UpdateInfoService w języku Kotlin lub ListenableFutureUpdateInfoService w języku 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());
}
}
Opcja B. Model pull (na żądanie)
W architekturze opartej na pobieraniu danych usługa obsługuje żądania odświeżania na żądanie wywoływane przez aplikacje klienckie, gdy lokalna pamięć podręczna jest nieaktualna.
Aby obsługiwać zapytania o aktualizacje na żądanie, rozszerz klasę UpdateInfoService w Kotlinie (implementując funkcję zawieszającą fetchUpdates()) lub ListenableFutureUpdateInfoService w Javie (implementując fetchUpdatesAsync() zwracającą obiekt Guava ListenableFuture):
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);
}
}
Krok 4. Wyczyść zastosowane aktualizacje po ponownym uruchomieniu urządzenia
UpdateInfoManager automatycznie usuwa przestarzałe aktualizacje, gdy tylko zostanie wywołana funkcja registerUpdate(). Po zakończeniu instalacji aktualizacji OTA program aktualizujący nie wywoła ponownie funkcji registerUpdate() aż do następnego zaplanowanego cyklu synchronizacji z serwerem. Aby zapobiec sytuacji, w której aplikacje klienckie widzą już zainstalowaną aktualizację jako nadal oczekującą natychmiast po ponownym uruchomieniu, nasłuchuj zdarzenia ACTION_BOOT_COMPLETED i wywołuj UpdateInfoManager.unregisterUpdate(), gdy aktualizacja OTA zostanie zainstalowana, aby wyczyścić rekord z pamięci podręcznej. Dzięki temu unikniesz bezwarunkowego usuwania oczekujących (niezainstalowanych) aktualizacji przy każdym normalnym ponownym uruchomieniu urządzenia. Ponieważ klucze UpdateInfoManager aktualizują rekordy według komponentu, podczas tworzenia obiektu UpdateInfo na potrzeby wyrejestrowania musisz podać tylko komponent docelowy:
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());
Krok 5. Sprawdź integrację
Aby sprawdzić integrację kompleksową i zapobiec typowym problemom z wdrażaniem OEM, na urządzeniu z Androidem lub emulatorze przeprowadź te testy za pomocą Android Debug Bridge (ADB):
Sprawdź, czy klienci ufają Twojemu dostawcy: aplikacje klienckie ignorują każdego dostawcę, który nie ma certyfikatu
READ_PRIVILEGED_PHONE_STATE, nawet jeśli jest preinstalowany. Sprawdź, czy uprawnienie zostało przyznane:adb shell dumpsys package <your_package_name> | grep "READ_PRIVILEGED_PHONE_STATE: granted=true"Następnie sprawdź, czy usługa jest wykrywalna i nie ma uprawnień do usługi. W danych wyjściowych usługi sprawdź symbole
exported=trueipermission=null:adb shell pm query-services --user 0 -a androidx.security.state.provider.UPDATE_INFO_SERVICEJeśli klient nadal nie widzi Twojego dostawcy, sprawdź w logcat komunikat
Ignoring untrusted update providerz taguSecurityPatchState.Sprawdź rozwiązanie intencji w profilu użytkownika 0 i w profilu służbowym: sprawdź, czy system operacyjny Android
PackageManagerrozwiązuje wyeksportowany filtr intencjiUPDATE_INFO_SERVICEzarówno w przypadku użytkownika podstawowego (User 0), jak i aktywnego profilu służbowego Androida Enterprise (np.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_SERVICESprawdź stan usługi i rekordy w pamięci podręcznej za pomocą
dumpsys:UpdateInfoServicezastępujedump(), aby zgłaszaćGlobal Last Check,Should Throttle(stan ogranicznika szybkości) iCached Updates. PonieważUpdateInfoServiceto powiązana usługa, a klienty odłączają się natychmiast po wysłaniu zapytania,dumpsys activity servicezwraca wartość(nothing), gdy żaden klient nie jest powiązany. Uruchom usługę jawnie przed uruchomieniemdumpsys: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>Przykładowe dane diagnostyczne:
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 2026Wywołaj powiązanie klienta i sprawdź wyniki telemetrii: w aplikacji testowej bez uprawnień (nieposiadającej uprawnień podpisu systemowego) wywołaj
SecurityPatchState.queryAllAvailableUpdates(). Jeśli zaimplementowano wywołania zwrotne telemetrii, sprawdź:- Sprawdź, czy klient bez uprawnień łączy się bez
SecurityExceptioni wywołujeonClientConnected(packageName, callerUid). - W przypadku dostawców korzystających z modelu push (
shouldFetchUpdates() == false): sprawdź, czyonRequestCompleted(telemetry)rejestrujeUpdateFetchOutcome.CACHE_HIT1z wartościąfetchDurationMillis == 0w przypadku każdego zapytania. - W przypadku dostawców korzystających z modelu pull (
shouldFetchUpdates() == true): sprawdź, czyonRequestCompleted(telemetry)rejestruje logiUpdateFetchOutcome.FETCHED(3) w przypadku początkowego zapytania o nieaktualną pamięć podręczną, a następnieCACHE_HIT(1) w przypadku kolejnych zapytań. (Aby zresetować 1-godzinny trwały ogranicznik szybkości między testami modelu Pull, uruchomadb shell pm clear <your_package_name>).
- Sprawdź, czy klient bez uprawnień łączy się bez
Konfiguracje opcjonalne i zaawansowane
Zasady pamięci podręcznej i ograniczanie liczby żądań
Gdy klient wysyła zapytanie o aktualizacje, UpdateInfoService wykonuje proces blokowania z podwójnym sprawdzaniem, aby zrównoważyć częstotliwość aktualizacji danych z obciążeniem serwera backendu:

- Szybka ścieżka (
shouldFetchUpdates()): domyślnieshouldFetchUpdates()zwracatrue(wskazującą nieaktualną pamięć podręczną) tylko wtedy, gdy globalna wartośćlastCheckTimeMillisjest starsza niż 1 godzina (TimeUnit.HOURS.toMillis(1)). GdyshouldFetchUpdates()zwracafalse, usługa natychmiast zwraca zapisane w pamięci podręcznej rekordy z wynikiemUpdateFetchOutcome.CACHE_HITbez uzyskiwania blokad i wykonywania operacji wejścia/wyjścia sieciowego. Możesz zastąpićshouldFetchUpdates(), aby dostosować tę zasadę buforowania. - Wolna ścieżka i łączenie żądań: gdy funkcja
shouldFetchUpdates()zwraca wartośćtrue, usługa uzyskuje wewnętrzny mutex współprogramu i ponownie oceniashouldFetchUpdates()(zwracając wartośćUpdateFetchOutcome.COALESCED, jeśli równoczesne żądanie odświeżyło już pamięć podręczną podczas oczekiwania na blokadę). - Trwały ogranicznik częstotliwości (
shouldThrottle()): aby chronić infrastrukturę backendu przed nagłymi wzrostami liczby zapytań lub powtarzającymi się błędami,shouldThrottle()wymusza trwały minimalny 1-godzinny interwał między ponownymi uruchomieniami aplikacji i urządzenia.UpdateInfoServicerejestruje każdą próbę przed wywołaniemfetchUpdates(), więc jeślifetchUpdates()zgłosi wyjątek (zwracającUpdateFetchOutcome.FAILEDpo wywołaniuonFetchFailed(e)), kolejne zapytania w ciągu następnych 60 minut będą zwracać dane rezerwowe z pamięci podręcznej z wynikiemUpdateFetchOutcome.THROTTLED.
Dostrzegalność, telemetria i diagnostyka
UpdateInfoService udostępnia wbudowane punkty obserwacji, które umożliwiają śledzenie wdrożenia klienta, monitorowanie opóźnień IPC i rejestrowanie błędów backendu bez instrumentowania stubów AIDL niskiego poziomu:
onRequestCompleted(telemetry): wywoływana po zakończeniu każdego sprawdzania aktualizacji zUpdateCheckTelemetrypodsumowaniem.onClientConnected(packageName, callerUid): wywoływana, gdy zweryfikowany klient otwiera sesję.onClientDisconnected(packageName, callerUid): wywoływana, gdy klient odłącza się lub jego proces zostaje zakończony.onFetchFailed(e): wywoływana, jeśli podczas wykonywania funkcjifetchUpdates()wystąpi wyjątek, zanim usługa zwróci dane rezerwowe z pamięci podręcznej.
Zastąp te wywołania zwrotne w klasie UpdateInfoService (Kotlin) lub 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);
}
}
Wyniki telemetryczne i dane o czasie oczekiwania
UpdateCheckTelemetry mierzy monotoniczne upływy czasu (SystemClock.elapsedRealtime()) i podaje jeden z 5 wyników zdefiniowanych w UpdateFetchOutcome:
| Stała wyniku | @IntDef Kod |
Właściwości danych rejestrowane | Opis i stan systemu |
|---|---|---|---|
UpdateFetchOutcome.CACHE_HIT |
1 |
totalDurationMillis, processingDurationMillis, callerUid |
Odpowiedź jest natychmiast pobierana z lokalnego dysku lub pamięci podręcznej pamięci RAM na ścieżce Fast Path (shouldFetchUpdates() zwrócono false). Wartości lockWaitDurationMillis i fetchDurationMillis to 0. |
UpdateFetchOutcome.COALESCED |
2 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid |
Zapytanie zostało umieszczone w kolejce za innym aktywnym odświeżaniem. Po uzyskaniu blokady dane były aktualne. Uniknięto duplikowania pobierania z sieci (fetchDurationMillis to 0). |
UpdateFetchOutcome.FETCHED |
3 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis, callerUid |
Synchronizacja sieci backendu została przeprowadzona (fetchUpdates() ukończono). Nowe rekordy zostały zapisane na dysku. |
UpdateFetchOutcome.THROTTLED |
4 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid |
Żądanie zostało zablokowane przez ogranicznik szybkości (zwrócono kod shouldThrottle() true). Bezpiecznie zwrócono do klienta dane z pamięci podręcznej (fetchDurationMillis to 0). |
UpdateFetchOutcome.FAILED |
5 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis, callerUid |
Sprawdzanie aktualizacji lub żądanie sieciowe spowodowało wyjątek. Został przechwycony przez zaporę sieciową wyjątku, uruchomiony onFetchFailed(e) i zwrócony z pamięci podręcznej. |
Zaawansowany punkt zaczepienia brokera usług: getCallerUid()
Gdy klient się połączy, UpdateInfoService automatycznie ocenia
getCallerUid() na początkowym wątku Binder (przed wywołaniem
Binder.clearCallingIdentity() przed fetchUpdates()), weryfikuje
własność pakietu i przekazuje zweryfikowany identyfikator UID wywołującego bezpośrednio do
onClientConnected(), onClientDisconnected() i telemetry.callerUid w onRequestCompleted(telemetry).
W przypadku standardowych komponentów Androida <service>nie musisz wywoływać ani zastępowaćgetCallerUid(). Metoda protected open getCallerUid() (która domyślnie deleguje do Binder.getCallingUid()) jest udostępniana jako punkt zastępowania dla aplikacji hosta, które kierują komunikację IPC za pomocą binder przez wewnętrzny broker usług lub architekturę proxy, co pozwala podklasie zwracać logiczny identyfikator UID klienta zamiast identyfikatora UID brokera.
Dodatkowe materiały
Więcej informacji o publikowaniu stanu zabezpieczeń znajdziesz w tych materiałach:
Dokumentacja
- Sprawdzanie stanu zabezpieczeń urządzenia
- Biuletyny bezpieczeństwa Androida
- Komponenty systemu modułowego
- Dodatkowe poprawki zabezpieczeń
- Informacje o wersji Security State Provider 1.0.0
Dokumentacja API
UpdateInfoServiceListenableFutureUpdateInfoServiceUpdateInfoManagerUpdateInfoUpdateInfo.BuilderUpdateCheckTelemetryUpdateFetchOutcomeSecurityPatchState