Jika Anda adalah produsen peralatan asli (OEM) atau mengelola klien update over-the-air (OTA) yang memiliki hak istimewa, Anda dapat memberikan visibilitas aplikasi yang sensitif terhadap keamanan di perangkat ke update keamanan yang tertunda sehingga aplikasi tersebut dapat mengevaluasi postur keamanan perangkat secara akurat. Untuk menerapkan kebijakan zero trust yang kuat, aplikasi harus dapat memverifikasi tidak hanya tingkat patch yang diinstal pada perangkat (Tingkat Patch Keamanan Perangkat, atau DSPL), tetapi juga update keamanan yang tersedia dan siap diinstal (Tingkat Patch Keamanan yang Tersedia, atau ASPL).
Karena aplikasi klien yang tidak memiliki hak istimewa tidak dapat langsung membaca properti firmware, memeriksa database updater pribadi, atau membuat kueri endpoint backend OEM internal, library AndroidX Security State Provider menyediakan arsitektur komunikasi antarproses (IPC) yang standar dan aman yang digunakan klien update untuk membagikan informasi tentang update yang tersedia. Dengan menerapkan
UpdateInfoService di klien update OTA, Anda dapat memublikasikan metadata ASPL untuk sistem tanpa mengekspos integrasi backend eksklusif. Meskipun
Google menyediakan penerapan untuk pengupdate komponen sistem modular (Mainline)
untuk perangkat GMS, perangkat non-GMS juga dapat memublikasikan metadata ASPL untuk
komponen sistem modular ini.
Ringkasan arsitektur
Diagram berikut menggambarkan cara library Penyedia Status Keamanan AndroidX menetapkan framework IPC yang aman dan standar antara aplikasi klien yang tidak memiliki hak istimewa dan layanan update di perangkat:

Model pengiriman data
Aplikasi klien mengkueri ketersediaan update dengan memanggil
queryAllAvailableUpdates atau
fetchAvailableSecurityPatchLevel. Di balik layar, pustaka
klien otomatis menemukan dan mengikat semua layanan terdaftar yang
memperluas class UpdateInfoService di perangkat dari aplikasi sistem yang
memiliki izin READ_PRIVILEGED_PHONE_STATE.
Seperti yang digambarkan dalam diagram sebelumnya, library security-state-provider
mendukung dua model penayangan data:
| Model pengiriman | Pemicu sinkronisasi | Respons klien | Kasus penggunaan yang direkomendasikan |
|---|---|---|---|
| Model push (Sinkronisasi latar belakang) | Pekerja latar belakang terjadwal (WorkManager atau JobScheduler) disinkronkan dengan backend Anda dan menulis rekaman ke UpdateInfoManager. Layanan Anda selalu ditayangkan dari cache disk lokal (shouldFetchUpdates() = false). |
Segera disajikan dari cache lokal. | Pengupdate OTA sistem OEM dan pengupdate komponen modular yang disinkronkan di latar belakang. |
| Tarik model (Sinkronisasi sesuai permintaan) | Kueri IPC klien masuk memicu pengambilan jaringan saat data yang di-cache sudah tidak berlaku (shouldFetchUpdates() = true). Penggabungan mutex dan pembatasan kecepatan (shouldThrottle()) melindungi backend Anda dari lonjakan. |
Menunggu pengambilan backend saat cache usang. | Pengupdate OTA OEM monolitik tanpa pekerja sinkronisasi latar belakang terjadwal. |
Beberapa penyedia update
Di perangkat Android produksi, beberapa penyedia update independen dapat berjalan bersamaan secara serentak. Misalnya, Mainline memublikasikan ketersediaan untuk komponen modular (COMPONENT_SYSTEM_MODULES), sementara klien OTA OEM Anda memublikasikan update untuk image OS utama (COMPONENT_SYSTEM).
Layanan Anda hanya perlu mendaftarkan update untuk komponen tertentu yang dikelolanya. Jika beberapa penyedia di perangkat memublikasikan update untuk komponen yang sama, aplikasi klien mengevaluasi tingkat patch tertinggi yang tersedia (menggunakan fetchAvailableSecurityPatchLevel()) atau memeriksa setiap catatan UpdateInfo (menggunakan queryAllAvailableUpdates()) untuk audit perusahaan. Pastikan layanan Anda selalu memublikasikan format kanonis untuk komponen Anda (DateBasedSecurityPatchLevel untuk COMPONENT_SYSTEM).
Panduan langkah demi langkah untuk mengaktifkan klien pembaruan Anda
Ikuti langkah-langkah berikut untuk mengintegrasikan library Penyedia Status Keamanan AndroidX ke dalam klien update Anda dan mulai memublikasikan ketersediaan update keamanan perangkat Anda.
Langkah 1: Tambahkan dependensi
Untuk menerapkan penyedia update, pastikan project Anda menyertakan repositori Maven Google, lalu tambahkan library security-state-provider ke file build.gradle.kts (Kotlin DSL) atau build.gradle (Groovy DSL) modul Anda:
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'
}
Langkah 2: Deklarasikan layanan update di manifes Anda
Deklarasikan layanan Anda di AndroidManifest.xml aplikasi Anda dengan
<intent-filter> yang cocok dengan
androidx.security.state.provider.UPDATE_INFO_SERVICE. Layanan harus
diekspor (android:exported="true") dan dikonfigurasi sebagai layanan pengguna tunggal
(android:singleUser="true") sehingga library klien dapat terikat padanya di seluruh
batas proses dan pengguna, terutama untuk profil kerja:
<!-- 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>
Jika pengupdate Anda tidak berjalan sebagai android.uid.system, deklarasikan juga izin berikut dalam manifes Anda dan pastikan untuk menambahkannya ke daftar izin hak istimewa yang diizinkan:
READ_PRIVILEGED_PHONE_STATE: diperlukan agar klien memercayai penyedia Anda.INTERACT_ACROSS_USERS: diperlukan untukandroid:singleUser="true".
Langkah 3: Terapkan UpdateInfoService
Untuk memublikasikan status update, Anda harus mengimplementasikan class UpdateInfoService
dan membuat catatan update yang cocok dengan model data yang diharapkan.
Spesifikasi model data UpdateInfo
Baik Anda memilih model Push atau model Pull, buat rekaman UpdateInfo
menggunakan UpdateInfo.Builder sesuai dengan
spesifikasi berikut:
| Nama Kolom | Metode Getter | Jenis Data | Persyaratan Validasi dan Format | Tujuan dan Semantik Sistem |
|---|---|---|---|---|
component |
getComponent() |
String (@Component) |
Konstanta kanonis di SecurityPatchState: COMPONENT_SYSTEM, COMPONENT_SYSTEM_MODULES, atau COMPONENT_KERNEL. |
Mengidentifikasi subsistem software atau firmware yang menjadi target update ini. |
securityPatchLevel |
getSecurityPatchLevel() |
SecurityPatchLevel |
Harus berupa instance DateBasedSecurityPatchLevel (YYYY-MM-DD) atau VersionedSecurityPatchLevel (major.minor.patch), atau diuraikan menggunakan SecurityPatchState.getComponentSecurityPatchLevel(). |
Tingkat patch keamanan target yang akan dicapai setelah update ini diinstal. |
publishedDateMillis |
getPublishedDateMillis() |
long |
Milidetik sejak epoch Unix (System.currentTimeMillis()). Harus > 0. |
Saat update tersedia untuk pengguna, seperti waktu rilis OTA. Jangan gunakan waktu download atau penginstalan payload. |
lastCheckTimeMillis |
getLastCheckTimeMillis() |
long |
Milidetik sejak epoch Unix. Harus berupa > 0. |
Stempel waktu saat penyedia Anda memverifikasi atau menemukan catatan pembaruan ini selama sinkronisasi. |
Pilih model pengiriman yang sesuai dengan arsitektur pengupdate Anda dari opsi berikut:
Opsi A: Model push (direkomendasikan)
Saat pekerja sinkronisasi latar belakang Anda memeriksa server OTA, validasi bahwa setiap update yang ditemukan meningkatkan tingkat patch perangkat saat ini dan pertahankan menggunakan UpdateInfoManager.registerUpdate(), atau panggil UpdateInfoManager.unregisterUpdate() jika tidak ada update keamanan yang lebih baru yang tertunda. Selalu panggil UpdateInfoManager.setLastCheckTimeMillis() di
akhir setiap sinkronisasi (bahkan setelah memanggil registerUpdate(), yang mempertahankan
catatan UpdateInfo per komponen, tetapi tidak memperbarui stempel waktu
pemeriksaan terakhir global yang ditampilkan kepada klien). Anda dapat menerapkannya dengan
WorkManager CoroutineWorker di Kotlin atau Worker di Java:
Kotlin
import android.content.Context
import androidx.security.state.SecurityPatchState
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel
import androidx.security.state.UpdateInfo
import androidx.security.state.provider.UpdateInfoManager
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import kotlin.math.max
class OtaSyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val updateInfoManager = UpdateInfoManager(applicationContext)
val securityPatchState = SecurityPatchState(applicationContext)
val currentSpl = securityPatchState.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM)
// 1. Fetch available update metadata from OEM backend
val latestUpdate = MyOtaClient.fetchLatestSystemUpdate()
val targetSplString = latestUpdate?.spl?.trim()
val targetSpl = if (!targetSplString.isNullOrEmpty()) {
DateBasedSecurityPatchLevel.fromString(targetSplString)
} else {
null
}
// 2. Defensively verify that target SPL is non-blank AND strictly newer than installed DSPL.
// If an update is a maintenance patch with no SPL increment (or if no update is available),
// unregister any stale cached record for this component.
if (latestUpdate != null && targetSpl != null && targetSpl > currentSpl) {
val updateInfo = UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(targetSpl)
.setPublishedDateMillis(latestUpdate.releaseTimeMillis)
.setLastCheckTimeMillis(System.currentTimeMillis())
.build()
updateInfoManager.registerUpdate(updateInfo)
} else {
val clearTarget = UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build()
updateInfoManager.unregisterUpdate(clearTarget)
}
// 3. Update global freshness timestamp (monotonic synchronization)
val currentCheckTime = System.currentTimeMillis()
val previousCheckTime = updateInfoManager.getLastCheckTimeMillis()
updateInfoManager.setLastCheckTimeMillis(max(previousCheckTime, currentCheckTime))
return Result.success()
}
}
Java
import android.content.Context;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.security.state.SecurityPatchState;
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel;
import androidx.security.state.SecurityPatchState.SecurityPatchLevel;
import androidx.security.state.UpdateInfo;
import androidx.security.state.provider.UpdateInfoManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
public class OtaSyncWorker extends Worker {
public OtaSyncWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
}
@NonNull
@Override
public Result doWork() {
// In Java, pass null for customSecurityState because UpdateInfoManager does not declare @JvmOverloads
UpdateInfoManager updateInfoManager =
new UpdateInfoManager(getApplicationContext(), /* customSecurityState= */ null);
SecurityPatchState securityPatchState = new SecurityPatchState(getApplicationContext());
SecurityPatchLevel currentSpl =
securityPatchState.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM);
// 1. Fetch available update metadata from OEM backend
MyOtaUpdate latestUpdate = MyOtaClient.fetchLatestSystemUpdate();
String targetSplString = (latestUpdate != null && latestUpdate.getSpl() != null)
? latestUpdate.getSpl().trim()
: null;
DateBasedSecurityPatchLevel targetSpl =
!TextUtils.isEmpty(targetSplString)
? DateBasedSecurityPatchLevel.fromString(targetSplString)
: null;
// 2. Defensively verify that target SPL is non-blank AND strictly newer than installed DSPL.
// If an update is a maintenance patch with no SPL increment (or if no update is available),
// unregister any stale cached record for this component.
if (latestUpdate != null && targetSpl != null && targetSpl.compareTo(currentSpl) > 0) {
UpdateInfo updateInfo = new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(targetSpl)
.setPublishedDateMillis(latestUpdate.getReleaseTimeMillis())
.setLastCheckTimeMillis(System.currentTimeMillis())
.build();
updateInfoManager.registerUpdate(updateInfo);
} else {
UpdateInfo clearTarget = new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build();
updateInfoManager.unregisterUpdate(clearTarget);
}
// 3. Update global freshness timestamp (monotonic synchronization)
long currentCheckTime = System.currentTimeMillis();
long previousCheckTime = updateInfoManager.getLastCheckTimeMillis();
updateInfoManager.setLastCheckTimeMillis(Math.max(previousCheckTime, currentCheckTime));
return Result.success();
}
}
Dalam model berbasis push, tugas latar belakang akan mempertahankan catatan update langsung ke
UpdateInfoManager. Untuk menginstruksikan framework agar selalu menayangkan data dari
penyimpanan disk lokal, ganti shouldFetchUpdates() agar menampilkan false dengan
memperluas UpdateInfoService di Kotlin atau
ListenableFutureUpdateInfoService di 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());
}
}
Opsi B: Model penarikan (on-demand)
Dalam arsitektur berbasis pull, layanan Anda menangani permintaan refresh sesuai permintaan yang dipicu oleh aplikasi klien saat cache lokal sudah tidak berlaku.
Untuk menangani kueri update sesuai permintaan, perluas UpdateInfoService di Kotlin
(menerapkan fungsi penangguhan fetchUpdates()) atau
ListenableFutureUpdateInfoService di Java (menerapkan
fetchUpdatesAsync() yang menampilkan ListenableFuture Guava):
Kotlin
package com.example.android.updater
import androidx.security.state.SecurityPatchState
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel
import androidx.security.state.UpdateInfo
import androidx.security.state.provider.UpdateInfoManager
import androidx.security.state.provider.UpdateInfoService
import java.util.concurrent.TimeUnit
class MyUpdateInfoService : UpdateInfoService() {
// Manage local update records and check timestamps
private val updateInfoManager by lazy { UpdateInfoManager(this) }
override suspend fun fetchUpdates(): List<UpdateInfo> {
val currentSpl = SecurityPatchState(this)
.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM)
// 1. Execute network request to OTA backend
val response = MyOtaBackendClient.checkAvailableUpdates()
// 2. Defensively filter out blank or non-advancing SPLs and map to UpdateInfo objects
val validUpdates = response.updates
.mapNotNull { updateItem ->
val splString = updateItem.targetSpl?.trim()
if (splString.isNullOrEmpty()) return@mapNotNull null
val parsedSpl = DateBasedSecurityPatchLevel.fromString(splString)
if (parsedSpl > currentSpl) {
UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(parsedSpl)
.setPublishedDateMillis(updateItem.releaseTimestampMillis)
.setLastCheckTimeMillis(System.currentTimeMillis())
.build()
} else {
null
}
}
// 3. If no advancing SYSTEM update is available (or if a previously offered update was revoked),
// proactively unregister any cached record for this component.
if (validUpdates.isEmpty()) {
val clearTarget = UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build()
updateInfoManager.unregisterUpdate(clearTarget)
}
return validUpdates
}
override fun shouldFetchUpdates(): Boolean {
// Enforce custom freshness threshold (for example, 4 hours instead of default 1 hour)
val lastCheckMillis = updateInfoManager.getLastCheckTimeMillis()
val dataAge = System.currentTimeMillis() - lastCheckMillis
return dataAge > TimeUnit.HOURS.toMillis(4)
}
}
Java
package com.example.android.updater;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.security.state.SecurityPatchState;
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel;
import androidx.security.state.SecurityPatchState.SecurityPatchLevel;
import androidx.security.state.UpdateInfo;
import androidx.security.state.provider.ListenableFutureUpdateInfoService;
import androidx.security.state.provider.UpdateInfoManager;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class MyUpdateInfoService extends ListenableFutureUpdateInfoService {
private UpdateInfoManager updateInfoManager;
@Override
public void onCreate() {
super.onCreate();
// Pass null for customSecurityState because UpdateInfoManager does not declare @JvmOverloads
updateInfoManager = new UpdateInfoManager(this, /* customSecurityState= */ null);
}
@NonNull
@Override
protected ListenableFuture<List<UpdateInfo>> fetchUpdatesAsync() {
try {
SecurityPatchLevel currentSpl = new SecurityPatchState(this)
.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM);
MyOtaBackendResponse response = MyOtaBackendClient.checkAvailableUpdates();
List<UpdateInfo> updates = new ArrayList<>();
for (MyOtaUpdateItem item : response.getUpdates()) {
String trimmedSpl = (item.getTargetSpl() != null) ? item.getTargetSpl().trim() : null;
if (!TextUtils.isEmpty(trimmedSpl)) {
DateBasedSecurityPatchLevel parsedSpl =
DateBasedSecurityPatchLevel.fromString(trimmedSpl);
if (parsedSpl.compareTo(currentSpl) > 0) {
updates.add(new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(parsedSpl)
.setPublishedDateMillis(item.getReleaseTimestampMillis())
.setLastCheckTimeMillis(System.currentTimeMillis())
.build());
}
}
}
// If no advancing SYSTEM update is available (or if a previously offered update was revoked),
// proactively unregister any cached record for this component.
if (updates.isEmpty()) {
UpdateInfo clearTarget = new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build();
updateInfoManager.unregisterUpdate(clearTarget);
}
return Futures.immediateFuture(updates);
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}
}
@Override
protected boolean shouldFetchUpdates() {
long lastCheckMillis = updateInfoManager.getLastCheckTimeMillis();
long dataAge = System.currentTimeMillis() - lastCheckMillis;
return dataAge > TimeUnit.HOURS.toMillis(4);
}
}
Langkah 4: Hapus update yang diterapkan setelah perangkat dimulai ulang
Meskipun UpdateInfoManager otomatis memangkas update yang sudah tidak berlaku setiap kali
registerUpdate() dipanggil, pengupdate Anda tidak akan memanggil
registerUpdate() lagi setelah update OTA selesai diinstal hingga siklus
sinkronisasi server terjadwal berikutnya. Untuk mencegah aplikasi klien melihat update yang sudah diinstal sebagai masih tertunda segera setelah dimulai ulang, dengarkan ACTION_BOOT_COMPLETED dan panggil UpdateInfoManager.unregisterUpdate() saat update OTA selesai diinstal untuk menghapus catatan dari cache lokal. Melakukan hal ini hanya saat
update selesai diinstal akan menghindari penghapusan tanpa syarat update yang tertunda
(diuninstal) pada setiap mulai ulang perangkat normal. Karena kunci
UpdateInfoManager memperbarui rekaman menurut
komponen, Anda hanya perlu menentukan komponen target saat membuat objek
UpdateInfo untuk pembatalan pendaftaran:
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());
Langkah 5: Verifikasi integrasi Anda
Jalankan pemeriksaan berikut di perangkat Android atau emulator menggunakan Android Debug Bridge (ADB) untuk memvalidasi integrasi menyeluruh dan mencegah potensi masalah umum saat men-deploy OEM:
Verifikasi bahwa klien memercayai penyedia Anda: Aplikasi klien mengabaikan penyedia mana pun yang tidak memiliki
READ_PRIVILEGED_PHONE_STATE, meskipun sudah diinstal sebelumnya. Pastikan izin diberikan:adb shell dumpsys package <your_package_name> | grep "READ_PRIVILEGED_PHONE_STATE: granted=true"Kemudian, konfirmasi bahwa layanan Anda dapat ditemukan dan tidak memiliki izin layanan. Pada output layanan Anda, periksa
exported=truedanpermission=null:adb shell pm query-services --user 0 -a androidx.security.state.provider.UPDATE_INFO_SERVICEJika klien masih tidak melihat penyedia Anda, periksa logcat untuk
Ignoring untrusted update providerdari tagSecurityPatchState.Verifikasi penyelesaian intent di Profil Pengguna 0 dan Profil Kerja: Pastikan Android OS
PackageManagermenyelesaikan filter intentUPDATE_INFO_SERVICEyang diekspor di pengguna utama (User 0) dan Profil Kerja Android Enterprise yang aktif (sepertiUser 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_SERVICEVerifikasi status layanan dan catatan yang di-cache menggunakan
dumpsys: PenggantianUpdateInfoServicedump()untuk melaporkanGlobal Last Check,Should Throttle(status pembatasan laju), danCached Updates. KarenaUpdateInfoServiceadalah layanan terikat dan klien segera melepaskan ikatan setelah membuat kueri,dumpsys activity serviceakan menampilkan(nothing)saat tidak ada klien yang terikat. Mulai layanan secara eksplisit sebelum menjalankandumpsys: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>Contoh output diagnostik:
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 2026Memicu pengikatan klien dan memverifikasi hasil telemetri: Dari aplikasi pengujian yang tidak memiliki hak istimewa (tidak memiliki izin tanda tangan sistem), panggil
SecurityPatchState.queryAllAvailableUpdates(). Jika Anda menerapkan callback telemetri, periksa hal berikut:- Verifikasi bahwa klien yang tidak memiliki hak istimewa terikat tanpa
SecurityExceptiondan memicuonClientConnected(packageName, callerUid). - Untuk penyedia model Push (
shouldFetchUpdates() == false): Pastikan bahwa logonRequestCompleted(telemetry)UpdateFetchOutcome.CACHE_HIT(1) denganfetchDurationMillis == 0di setiap kueri. - Untuk penyedia model Pull (
shouldFetchUpdates() == true): Verifikasi bahwaonRequestCompleted(telemetry)mencatatUpdateFetchOutcome.FETCHED(3) pada kueri cache usang awal, diikuti denganCACHE_HIT(1) pada kueri berikutnya yang langsung dilakukan. (Untuk mereset pembatas kecepatan persisten 1 jam di antara uji coba model Pull, jalankanadb shell pm clear <your_package_name>.)
- Verifikasi bahwa klien yang tidak memiliki hak istimewa terikat tanpa
Konfigurasi opsional dan lanjutan
Kebijakan caching dan pembatasan kapasitas
Saat klien mengkueri update, UpdateInfoService menjalankan alur kerja penguncian yang diperiksa ganda untuk menyeimbangkan keaktualan data dengan beban server backend:

- Jalur Cepat (
shouldFetchUpdates()): Secara default,shouldFetchUpdates()menampilkantrue(menunjukkan cache yang tidak berlaku) hanya jikalastCheckTimeMillisglobal lebih lama dari 1 jam (TimeUnit.HOURS.toMillis(1)). JikashouldFetchUpdates()menampilkanfalse, layanan akan segera menampilkan rekaman yang di-cache dengan hasilUpdateFetchOutcome.CACHE_HITtanpa mendapatkan kunci atau melakukan I/O jaringan. Anda dapat menggantishouldFetchUpdates()untuk menyesuaikan kebijakan penyimpanan dalam cache ini. - Jalur Lambat & Penggabungan Permintaan: Saat
shouldFetchUpdates()menampilkantrue, layanan akan mendapatkan mutex coroutine internal dan mengevaluasi ulangshouldFetchUpdates()(menampilkanUpdateFetchOutcome.COALESCEDjika permintaan serentak telah memperbarui cache saat menunggu kunci). - Pembatasan Kapasitas Persisten (
shouldThrottle()): Untuk melindungi infrastruktur backend dari burst kueri atau kegagalan berulang,shouldThrottle()menerapkan interval minimum 1 jam yang persisten di seluruh aplikasi dan mulai ulang perangkat.UpdateInfoServicemencatat setiap upaya sebelum memanggilfetchUpdates(), jadi jikafetchUpdates()menampilkan pengecualian (menampilkanUpdateFetchOutcome.FAILEDsetelah memanggilonFetchFailed(e)), kueri berikutnya selama 60 menit ke depan akan menampilkan data penggantian yang di-cache dengan baik dengan hasilUpdateFetchOutcome.THROTTLED.
Kemampuan observasi, telemetri, dan diagnostik
UpdateInfoService menyediakan hook observabilitas bawaan untuk melacak adopsi klien, memantau latensi IPC, dan mencatat error backend tanpa menginstrumentasi stub AIDL tingkat rendah:
onRequestCompleted(telemetry): Dipanggil setelah setiap pemeriksaan update selesai dengan ringkasanUpdateCheckTelemetry.onClientConnected(packageName, callerUid): Dipanggil saat klien terverifikasi membuka sesi.onClientDisconnected(packageName, callerUid): Dipanggil saat klien melepaskan binding atau prosesnya berakhir.onFetchFailed(e): Dipanggil jika terjadi pengecualian selamafetchUpdates(), sebelum layanan menampilkan data penggantian yang di-cache.
Ganti callback ini di UpdateInfoService (Kotlin) atau
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);
}
}
Hasil telemetri dan metrik latensi
UpdateCheckTelemetry mengukur durasi yang berlalu secara monoton
(SystemClock.elapsedRealtime()) dan melaporkan salah satu dari lima hasil yang ditentukan
dalam UpdateFetchOutcome:
| Konstanta Hasil | @IntDef Code |
Properti Metrik yang Dicatat | Deskripsi & Status Sistem |
|---|---|---|---|
UpdateFetchOutcome.CACHE_HIT |
1 |
totalDurationMillis, processingDurationMillis, callerUid |
Langsung ditayangkan dari cache disk/cache memori lokal di Jalur Cepat (shouldFetchUpdates() menampilkan false). lockWaitDurationMillis dan fetchDurationMillis adalah 0. |
UpdateFetchOutcome.COALESCED |
2 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid |
Kueri diantrekan di belakang pembaruan aktif lainnya; setelah mendapatkan kunci, data sudah baru. Menghindari pengambilan jaringan duplikat (fetchDurationMillis adalah 0). |
UpdateFetchOutcome.FETCHED |
3 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis, callerUid |
Sinkronisasi jaringan backend berhasil dieksekusi (fetchUpdates() selesai). Data baru disimpan ke disk. |
UpdateFetchOutcome.THROTTLED |
4 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid |
Permintaan diblokir oleh pembatas kecepatan (shouldThrottle() menampilkan true). Data yang di-cache ditampilkan dengan aman ke klien (fetchDurationMillis adalah 0). |
UpdateFetchOutcome.FAILED |
5 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis, callerUid |
Pemeriksaan update atau permintaan jaringan memunculkan pengecualian. Ditangkap oleh firewall pengecualian, diaktifkan onFetchFailed(e), menampilkan penggantian cache. |
Hook broker layanan lanjutan: getCallerUid()
Saat klien terhubung, UpdateInfoService akan otomatis mengevaluasi
getCallerUid() di thread Binder awal (sebelum memanggil
Binder.clearCallingIdentity() sebelum fetchUpdates()), memverifikasi
kepemilikan paket, dan meneruskan UID pemanggil yang diverifikasi langsung ke
onClientConnected(), onClientDisconnected(), dan telemetry.callerUid di
onRequestCompleted(telemetry).
Untuk komponen <service> Android standar, Anda tidak perlu memanggil atau mengganti
getCallerUid(). Metode protected open getCallerUid() (yang didelegasikan ke
Binder.getCallingUid() secara default) disediakan sebagai hook penggantian untuk
aplikasi host yang merutekan Binder IPC melalui perantara layanan internal atau
arsitektur proxy, sehingga memungkinkan subclass menampilkan UID klien logis
daripada UID broker.
Referensi lainnya
Untuk mengetahui informasi selengkapnya tentang memublikasikan status keamanan, lihat referensi berikut:
Dokumentasi
- Memahami status keamanan perangkat
- Buletin Keamanan Android
- Komponen sistem modular
- Patch keamanan tambahan
- Catatan rilis Security State Provider 1.0.0
Referensi API
UpdateInfoServiceListenableFutureUpdateInfoServiceUpdateInfoManagerUpdateInfoUpdateInfo.BuilderUpdateCheckTelemetryUpdateFetchOutcomeSecurityPatchState