Publikowanie stanu zabezpieczeń urządzenia

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:

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 przypadku android: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):

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

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

    Jeśli klient nadal nie widzi Twojego dostawcy, sprawdź w logcat komunikat Ignoring untrusted update provider z tagu SecurityPatchState.

  2. Sprawdź rozwiązanie intencji w profilu użytkownika 0 i w profilu służbowym: sprawdź, czy system operacyjny Android PackageManager rozwiązuje wyeksportowany filtr intencji UPDATE_INFO_SERVICE zaró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_SERVICE
    
  3. Sprawdź stan usługi i rekordy w pamięci podręcznej za pomocą dumpsys: UpdateInfoService zastępuje dump(), aby zgłaszać Global Last Check, Should Throttle (stan ogranicznika szybkości) i Cached Updates. Ponieważ UpdateInfoService to powiązana usługa, a klienty odłączają się natychmiast po wysłaniu zapytania, dumpsys activity service zwraca wartość (nothing), gdy żaden klient nie jest powiązany. Uruchom usługę jawnie przed uruchomieniem dumpsys:

    adb shell am start-service -a androidx.security.state.provider.UPDATE_INFO_SERVICE <your_package_name>/.<service_class_name>
    adb shell dumpsys activity service <your_package_name>/.<service_class_name>
    

    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 2026
    
  4. Wywoł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 SecurityException i wywołuje onClientConnected(packageName, callerUid).
    • W przypadku dostawców korzystających z modelu push (shouldFetchUpdates() == false): sprawdź, czyonRequestCompleted(telemetry) rejestruje UpdateFetchOutcome.CACHE_HIT1 z wartością fetchDurationMillis == 0 w przypadku każdego zapytania.
    • W przypadku dostawców korzystających z modelu pull (shouldFetchUpdates() == true): sprawdź, czy onRequestCompleted(telemetry) rejestruje logi UpdateFetchOutcome.FETCHED (3) w przypadku początkowego zapytania o nieaktualną pamięć podręczną, a następnie CACHE_HIT (1) w przypadku kolejnych zapytań. (Aby zresetować 1-godzinny trwały ogranicznik szybkości między testami modelu Pull, uruchom adb shell pm clear <your_package_name>).

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:

Usługa UpdateInfoService wykonuje przepływ pracy z podwójnym sprawdzaniem blokady, aby zrównoważyć częstotliwość aktualizacji danych z obciążeniem serwera backendu.

  • Szybka ścieżka (shouldFetchUpdates()): domyślnie shouldFetchUpdates() zwraca true (wskazującą nieaktualną pamięć podręczną) tylko wtedy, gdy globalna wartość lastCheckTimeMillis jest starsza niż 1 godzina (TimeUnit.HOURS.toMillis(1)). Gdy shouldFetchUpdates() zwraca false, usługa natychmiast zwraca zapisane w pamięci podręcznej rekordy z wynikiem UpdateFetchOutcome.CACHE_HIT bez 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 ocenia shouldFetchUpdates() (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. UpdateInfoService rejestruje każdą próbę przed wywołaniem fetchUpdates(), więc jeśli fetchUpdates() zgłosi wyjątek (zwracając UpdateFetchOutcome.FAILED po wywołaniu onFetchFailed(e)), kolejne zapytania w ciągu następnych 60 minut będą zwracać dane rezerwowe z pamięci podręcznej z wynikiem UpdateFetchOutcome.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:

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

Dokumentacja API