Memublikasikan status keamanan perangkat

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:

Library AndroidX Security State Provider membuat framework IPC yang terstandarisasi dan aman 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 untuk android: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:

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

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

    Jika klien masih tidak melihat penyedia Anda, periksa logcat untuk Ignoring untrusted update provider dari tag SecurityPatchState.

  2. Verifikasi penyelesaian intent di Profil Pengguna 0 dan Profil Kerja: Pastikan Android OS PackageManager menyelesaikan filter intent UPDATE_INFO_SERVICE yang diekspor di pengguna utama (User 0) dan Profil Kerja Android Enterprise yang aktif (seperti 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. Verifikasi status layanan dan catatan yang di-cache menggunakan dumpsys: Penggantian UpdateInfoService dump() untuk melaporkan Global Last Check, Should Throttle (status pembatasan laju), dan Cached Updates. Karena UpdateInfoService adalah layanan terikat dan klien segera melepaskan ikatan setelah membuat kueri, dumpsys activity service akan menampilkan (nothing) saat tidak ada klien yang terikat. Mulai layanan secara eksplisit sebelum menjalankan 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>
    

    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 2026
    
  4. Memicu 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 SecurityException dan memicu onClientConnected(packageName, callerUid).
    • Untuk penyedia model Push (shouldFetchUpdates() == false): Pastikan bahwa log onRequestCompleted(telemetry) UpdateFetchOutcome.CACHE_HIT (1) dengan fetchDurationMillis == 0 di setiap kueri.
    • Untuk penyedia model Pull (shouldFetchUpdates() == true): Verifikasi bahwa onRequestCompleted(telemetry) mencatat UpdateFetchOutcome.FETCHED (3) pada kueri cache usang awal, diikuti dengan CACHE_HIT (1) pada kueri berikutnya yang langsung dilakukan. (Untuk mereset pembatas kecepatan persisten 1 jam di antara uji coba model Pull, jalankan adb shell pm clear <your_package_name>.)

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:

UpdateInfoService menjalankan alur kerja penguncian yang diperiksa dua kali untuk menyeimbangkan keaktualan data dengan beban server backend

  • Jalur Cepat (shouldFetchUpdates()): Secara default, shouldFetchUpdates() menampilkan true (menunjukkan cache yang tidak berlaku) hanya jika lastCheckTimeMillis global lebih lama dari 1 jam (TimeUnit.HOURS.toMillis(1)). Jika shouldFetchUpdates() menampilkan false, layanan akan segera menampilkan rekaman yang di-cache dengan hasil UpdateFetchOutcome.CACHE_HIT tanpa mendapatkan kunci atau melakukan I/O jaringan. Anda dapat mengganti shouldFetchUpdates() untuk menyesuaikan kebijakan penyimpanan dalam cache ini.
  • Jalur Lambat & Penggabungan Permintaan: Saat shouldFetchUpdates() menampilkan true, layanan akan mendapatkan mutex coroutine internal dan mengevaluasi ulang shouldFetchUpdates() (menampilkan UpdateFetchOutcome.COALESCED jika 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. UpdateInfoService mencatat setiap upaya sebelum memanggil fetchUpdates(), jadi jika fetchUpdates() menampilkan pengecualian (menampilkan UpdateFetchOutcome.FAILED setelah memanggil onFetchFailed(e)), kueri berikutnya selama 60 menit ke depan akan menampilkan data penggantian yang di-cache dengan baik dengan hasil UpdateFetchOutcome.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:

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

Referensi API