Công bố trạng thái bảo mật của thiết bị

Nếu là nhà sản xuất thiết bị gốc (OEM) hoặc duy trì ứng dụng cập nhật qua mạng (OTA) có đặc quyền, bạn có thể cấp cho các ứng dụng nhạy cảm về bảo mật trên thiết bị quyền xem các bản cập nhật bảo mật đang chờ xử lý để các ứng dụng này có thể đánh giá chính xác trạng thái bảo mật của thiết bị. Để thực thi các chính sách không tin tưởng mạnh mẽ, các ứng dụng phải có khả năng xác minh không chỉ cấp bản vá được cài đặt trên thiết bị (Cấp bản vá bảo mật thiết bị hoặc DSPL), mà còn cả những bản cập nhật bảo mật có sẵn và sẵn sàng cài đặt (Cấp bản vá bảo mật có sẵn hoặc ASPL).

Vì các ứng dụng khách không có đặc quyền không thể đọc trực tiếp các thuộc tính của chương trình cơ sở, kiểm tra cơ sở dữ liệu riêng tư của trình cập nhật hoặc truy vấn các điểm cuối phụ trợ nội bộ của OEM, nên thư viện AndroidX Security State Provider cung cấp một cấu trúc giao tiếp giữa các tiến trình (IPC) an toàn, được tiêu chuẩn hoá mà các ứng dụng cập nhật sử dụng để chia sẻ thông tin về các bản cập nhật hiện có. Bằng cách triển khai UpdateInfoService trong ứng dụng cập nhật qua mạng (OTA), bạn có thể xuất bản siêu dữ liệu ASPL cho hệ thống mà không cần tiết lộ các hoạt động tích hợp phụ trợ độc quyền. Mặc dù Google cung cấp quy trình triển khai cho trình cập nhật thành phần hệ thống theo mô-đun (Mainline) cho các thiết bị GMS, nhưng các thiết bị không phải GMS cũng có thể xuất bản siêu dữ liệu ASPL cho các thành phần hệ thống theo mô-đun này.

Tổng quan về kiến trúc

Sơ đồ sau đây minh hoạ cách thư viện Trình cung cấp trạng thái bảo mật AndroidX thiết lập một khung IPC bảo mật, được tiêu chuẩn hoá giữa các ứng dụng khách không có đặc quyền và các dịch vụ cập nhật trên thiết bị:

Thư viện Trình cung cấp trạng thái bảo mật AndroidX thiết lập một khung IPC bảo mật, được chuẩn hoá giữa các ứng dụng khách không có đặc quyền và các dịch vụ cập nhật trên thiết bị

Mô hình phân phối dữ liệu

Các ứng dụng khách truy vấn tình trạng cung cấp bản cập nhật bằng cách gọi queryAllAvailableUpdates hoặc fetchAvailableSecurityPatchLevel. Về cơ bản, thư viện ứng dụng sẽ tự động phát hiện và liên kết với tất cả các dịch vụ đã đăng ký mở rộng lớp UpdateInfoService trên thiết bị từ các ứng dụng hệ thống có quyền READ_PRIVILEGED_PHONE_STATE.

Như minh hoạ trong sơ đồ trước đó, thư viện security-state-provider hỗ trợ 2 mô hình phân phối dữ liệu:

Mô hình phân phối Điều kiện kích hoạt đồng bộ hoá Phản hồi của khách hàng Các trường hợp sử dụng được đề xuất
Mô hình đẩy (Đồng bộ hoá ở chế độ nền) Trình chạy ở chế độ nền theo lịch (WorkManager hoặc JobScheduler) đồng bộ hoá với phần phụ trợ của bạn và ghi các bản ghi vào UpdateInfoManager. Dịch vụ của bạn luôn phân phát từ bộ nhớ đệm của ổ đĩa cục bộ (shouldFetchUpdates() = false). Được phân phát ngay từ bộ nhớ đệm cục bộ. Trình cập nhật OTA hệ thống của OEM và trình cập nhật thành phần mô-đun được đồng bộ hoá ở chế độ nền.
Mô hình kéo (Đồng bộ hoá theo yêu cầu) Các truy vấn IPC của ứng dụng nhận được sẽ kích hoạt quá trình tìm nạp mạng khi các bản ghi được lưu vào bộ nhớ đệm đã cũ (shouldFetchUpdates() = true). Việc kết hợp mutex và giới hạn tốc độ (shouldThrottle()) giúp bảo vệ phần phụ trợ của bạn khỏi các đột biến. Chờ quá trình tìm nạp phụ trợ khi bộ nhớ đệm đã lỗi thời. Trình cập nhật OTA của OEM nguyên khối không có trình chạy đồng bộ hoá ở chế độ nền theo lịch.

Nhiều nhà cung cấp bản cập nhật

Trên các thiết bị Android phát hành công khai, nhiều nhà cung cấp dịch vụ cập nhật độc lập cùng tồn tại đồng thời. Ví dụ: Mainline công bố trạng thái sẵn có cho các thành phần theo mô-đun (COMPONENT_SYSTEM_MODULES), trong khi ứng dụng OTA của OEM công bố các bản cập nhật cho hình ảnh hệ điều hành chính (COMPONENT_SYSTEM).

Dịch vụ của bạn chỉ cần đăng ký các bản cập nhật cho những thành phần cụ thể mà dịch vụ đó quản lý. Nếu nhiều nhà cung cấp trên một thiết bị phát hành bản cập nhật cho cùng một thành phần, thì các ứng dụng khách sẽ đánh giá cấp độ bản vá cao nhất hiện có (bằng cách sử dụng fetchAvailableSecurityPatchLevel()) hoặc kiểm tra từng bản ghi UpdateInfo (bằng cách sử dụng queryAllAvailableUpdates()) để kiểm tra doanh nghiệp. Đảm bảo rằng dịch vụ của bạn luôn xuất bản định dạng chuẩn cho thành phần (DateBasedSecurityPatchLevel cho COMPONENT_SYSTEM).

Hướng dẫn từng bước để tích hợp ứng dụng cập nhật

Làm theo các bước sau để tích hợp thư viện AndroidX Security State Provider vào ứng dụng cập nhật và bắt đầu xuất bản thông tin về khả năng cung cấp bản cập nhật bảo mật của thiết bị.

Bước 1: Thêm phần phụ thuộc

Để triển khai một trình cung cấp bản cập nhật, hãy đảm bảo dự án của bạn có kho lưu trữ Maven của Google, sau đó thêm thư viện security-state-provider vào tệp build.gradle.kts (Kotlin DSL) hoặc build.gradle (Groovy DSL) của mô-đun:

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'
}

Bước 2: Khai báo dịch vụ cập nhật trong tệp kê khai

Khai báo dịch vụ trong AndroidManifest.xml của ứng dụng bằng <intent-filter> khớp với androidx.security.state.provider.UPDATE_INFO_SERVICE. Dịch vụ phải được xuất (android:exported="true") và định cấu hình dưới dạng dịch vụ một người dùng (android:singleUser="true") để thư viện ứng dụng có thể liên kết với dịch vụ đó trên các ranh giới quy trình và người dùng, đặc biệt là đối với hồ sơ công việc:

<!-- 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>

Nếu trình cập nhật của bạn không chạy dưới dạng android.uid.system, hãy khai báo cả các quyền sau trong tệp kê khai và nhớ thêm các quyền đó vào danh sách cho phép quyền đặc biệt:

  • READ_PRIVILEGED_PHONE_STATE: bắt buộc để các ứng dụng tin tưởng nhà cung cấp của bạn.
  • INTERACT_ACROSS_USERS: bắt buộc đối với android:singleUser="true".

Bước 3: Triển khai UpdateInfoService

Để xuất bản trạng thái cập nhật, bạn phải triển khai lớp UpdateInfoService và tạo các bản ghi cập nhật khớp với mô hình dữ liệu dự kiến.

Quy cách mô hình dữ liệu UpdateInfo

Cho dù bạn chọn mô hình Push hay Pull, hãy tạo bản ghi UpdateInfo bằng UpdateInfo.Builder theo quy cách sau:

Tên trường Phương thức Getter Loại dữ liệu Yêu cầu về xác thực và định dạng Mục đích và ngữ nghĩa hệ thống
component getComponent() String (@Component) Hằng số chính tắc trong SecurityPatchState: COMPONENT_SYSTEM, COMPONENT_SYSTEM_MODULES hoặc COMPONENT_KERNEL. Xác định hệ thống con phần mềm hoặc chương trình cơ sở mà bản cập nhật này nhắm đến.
securityPatchLevel getSecurityPatchLevel() SecurityPatchLevel Phải là một phiên bản của DateBasedSecurityPatchLevel (YYYY-MM-DD) hoặc VersionedSecurityPatchLevel (major.minor.patch) hoặc được phân tích cú pháp bằng SecurityPatchState.getComponentSecurityPatchLevel(). Cấp bản vá bảo mật mục tiêu sẽ đạt được sau khi cài đặt bản cập nhật này.
publishedDateMillis getPublishedDateMillis() long Số mili giây kể từ thời gian bắt đầu của hệ thống Unix (System.currentTimeMillis()). Phải là > 0. Thời điểm bản cập nhật được cung cấp cho người dùng, chẳng hạn như thời gian phát hành OTA. Đừng sử dụng thời gian tải xuống hoặc cài đặt tải trọng.
lastCheckTimeMillis getLastCheckTimeMillis() long Số mili giây kể từ thời gian bắt đầu của hệ thống Unix. Phải là > 0. Dấu thời gian khi nhà cung cấp của bạn xác minh hoặc phát hiện bản ghi cập nhật này trong quá trình đồng bộ hoá.

Chọn mô hình phân phối phù hợp với cấu trúc của trình cập nhật trong số các lựa chọn sau:

Cách A: Mô hình truyền dữ liệu (nên dùng)

Khi trình chạy đồng bộ hoá ở chế độ nền kiểm tra máy chủ OTA, hãy xác thực rằng mọi bản cập nhật được phát hiện đều nâng cấp cấp độ bản vá hiện tại của thiết bị và duy trì cấp độ đó bằng cách sử dụng UpdateInfoManager.registerUpdate() hoặc gọi UpdateInfoManager.unregisterUpdate() nếu không có bản cập nhật bảo mật nào đang chờ xử lý. Luôn gọi UpdateInfoManager.setLastCheckTimeMillis() ở cuối mỗi lần đồng bộ hoá (ngay cả sau khi gọi registerUpdate(), thao tác này sẽ duy trì bản ghi UpdateInfo trên mỗi thành phần nhưng không cập nhật dấu thời gian kiểm tra gần đây nhất trên toàn cầu được trả về cho các ứng dụng). Bạn có thể triển khai việc này bằng WorkManager CoroutineWorker trong Kotlin hoặc Worker trong 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();
    }
}

Trong mô hình dựa trên thông báo đẩy, các tác vụ ở chế độ nền sẽ duy trì các bản ghi cập nhật trực tiếp đến UpdateInfoManager. Để hướng dẫn khung luôn phân phát các bản ghi từ bộ nhớ trên ổ đĩa cục bộ, hãy ghi đè shouldFetchUpdates() để trả về false bằng cách mở rộng UpdateInfoService trong Kotlin hoặc ListenableFutureUpdateInfoService trong 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());
    }
}

Cách B: Mô hình kéo (theo yêu cầu)

Trong cấu trúc dựa trên thao tác kéo, dịch vụ của bạn xử lý các yêu cầu làm mới theo yêu cầu do ứng dụng khách kích hoạt khi bộ nhớ đệm cục bộ đã lỗi thời.

Để xử lý các truy vấn cập nhật theo yêu cầu, hãy mở rộng UpdateInfoService trong Kotlin (triển khai hàm tạm ngưng fetchUpdates()) hoặc ListenableFutureUpdateInfoService trong Java (triển khai fetchUpdatesAsync() trả về ListenableFuture của 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);
    }
}

Bước 4: Xoá các bản cập nhật đã áp dụng sau khi khởi động lại thiết bị

Mặc dù UpdateInfoManager tự động cắt tỉa các bản cập nhật không còn dùng nữa bất cứ khi nào registerUpdate() được gọi, nhưng trình cập nhật của bạn sẽ không gọi lại registerUpdate() sau khi quá trình cài đặt bản cập nhật OTA hoàn tất cho đến chu kỳ đồng bộ hoá máy chủ theo lịch tiếp theo. Để ngăn các ứng dụng khách coi một bản cập nhật đã cài đặt là vẫn đang chờ xử lý ngay sau khi khởi động lại, hãy theo dõi ACTION_BOOT_COMPLETED và gọi UpdateInfoManager.unregisterUpdate() khi bản cập nhật OTA đã cài đặt xong để xoá bản ghi khỏi bộ nhớ đệm cục bộ. Chỉ thực hiện việc này khi một bản cập nhật đã cài đặt xong để tránh xoá vô điều kiện các bản cập nhật đang chờ xử lý (chưa cài đặt) trên mọi lần khởi động lại thiết bị thông thường. Vì các khoá UpdateInfoManager cập nhật các bản ghi theo thành phần, nên bạn chỉ cần chỉ định thành phần đích khi tạo đối tượng UpdateInfo để huỷ đăng ký:

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());

Bước 5: Xác minh quá trình tích hợp

Chạy các quy trình kiểm tra sau trên một thiết bị Android hoặc trình mô phỏng bằng Cầu gỡ lỗi Android (ADB) để xác thực quá trình tích hợp đầu cuối và ngăn chặn các lỗi triển khai phổ biến của OEM:

  1. Xác minh rằng các ứng dụng khách tin tưởng nhà cung cấp của bạn: Các ứng dụng khách sẽ bỏ qua mọi nhà cung cấp không giữ READ_PRIVILEGED_PHONE_STATE, ngay cả khi nhà cung cấp đó được cài đặt sẵn. Xác nhận rằng bạn đã cấp quyền:

    adb shell dumpsys package <your_package_name> | grep "READ_PRIVILEGED_PHONE_STATE: granted=true"
    

    Sau đó, hãy xác nhận rằng dịch vụ của bạn có thể tìm thấy và không có quyền truy cập dịch vụ. Trong kết quả đầu ra của dịch vụ, hãy kiểm tra exported=true và permission=null:

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

    Nếu khách hàng vẫn không thấy nhà cung cấp của bạn, hãy kiểm tra logcat để tìm Ignoring untrusted update provider trong thẻ SecurityPatchState.

  2. Xác minh việc phân giải ý định ở cả Người dùng 0 và Hồ sơ công việc: Xác nhận rằng hệ điều hành Android PackageManager phân giải bộ lọc ý định UPDATE_INFO_SERVICE đã xuất của bạn ở cả người dùng chính (User 0) và mọi Hồ sơ công việc Android Enterprise đang hoạt động (chẳng hạn như 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. Xác minh trạng thái dịch vụ và các bản ghi được lưu vào bộ nhớ đệm bằng dumpsys: UpdateInfoService ghi đè dump() để báo cáo Global Last Check, Should Throttle (trạng thái của cơ chế giới hạn tốc độ) và Cached Updates. Vì UpdateInfoService là một dịch vụ ràng buộc và các ứng dụng sẽ huỷ liên kết ngay sau khi truy vấn, nên dumpsys activity service sẽ xuất (nothing) khi không có ứng dụng nào được liên kết. Bắt đầu dịch vụ một cách rõ ràng trước khi chạy 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>
    

    Ví dụ về kết quả chẩn đoán:

    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. Kích hoạt liên kết máy khách và xác minh kết quả đo từ xa: Từ một ứng dụng kiểm thử không có đặc quyền (không có quyền chữ ký hệ thống), hãy gọi SecurityPatchState.queryAllAvailableUpdates(). Nếu bạn đã triển khai các lệnh gọi lại đo từ xa, hãy kiểm tra những điều sau:

    • Xác minh rằng ứng dụng không có đặc quyền liên kết mà không có SecurityException và kích hoạt onClientConnected(packageName, callerUid).
    • Đối với nhà cung cấp mô hình Push (shouldFetchUpdates() == false): Xác minh rằng nhật ký onRequestCompleted(telemetry) UpdateFetchOutcome.CACHE_HIT (1) có fetchDurationMillis == 0 trên mọi truy vấn.
    • Đối với nhà cung cấp mô hình Kéo (shouldFetchUpdates() == true): Xác minh rằng nhật ký onRequestCompleted(telemetry) UpdateFetchOutcome.FETCHED (3) trên truy vấn bộ nhớ đệm cũ ban đầu, theo sau là CACHE_HIT (1) trên các truy vấn tiếp theo ngay lập tức. (Để đặt lại bộ giới hạn tốc độ liên tục trong 1 giờ giữa các lần chạy thử nghiệm theo mô hình Kéo, hãy chạy adb shell pm clear <your_package_name>.)

Cấu hình nâng cao và không bắt buộc

Chính sách lưu vào bộ nhớ đệm và giới hạn tốc độ

Khi một ứng dụng truy vấn thông tin cập nhật, UpdateInfoService sẽ thực thi quy trình khoá được kiểm tra hai lần để cân bằng độ mới của dữ liệu với tải máy chủ phụ trợ:

UpdateInfoService thực thi quy trình khoá được kiểm tra hai lần để cân bằng độ mới của dữ liệu với tải máy chủ phụ trợ

  • Đường dẫn nhanh (shouldFetchUpdates()): Theo mặc định, shouldFetchUpdates() chỉ trả về true (cho biết bộ nhớ đệm cũ) khi lastCheckTimeMillis toàn cầu đã quá 1 giờ (TimeUnit.HOURS.toMillis(1)). Khi shouldFetchUpdates() trả về false, dịch vụ sẽ trả về ngay các bản ghi được lưu vào bộ nhớ đệm với kết quả UpdateFetchOutcome.CACHE_HIT mà không cần lấy khoá hoặc thực hiện I/O mạng. Bạn có thể ghi đè shouldFetchUpdates() để tuỳ chỉnh chính sách lưu vào bộ nhớ đệm này.
  • Đường dẫn chậm và hợp nhất yêu cầu: Khi shouldFetchUpdates() trả về true, dịch vụ sẽ có được một mutex coroutine nội bộ và đánh giá lại shouldFetchUpdates() (trả về UpdateFetchOutcome.COALESCED nếu một yêu cầu đồng thời đã làm mới bộ nhớ đệm trong khi chờ khoá).
  • Bộ giới hạn tốc độ liên tục (shouldThrottle()): Để bảo vệ cơ sở hạ tầng phụ trợ khỏi các đợt truy vấn hoặc lỗi lặp lại, shouldThrottle() sẽ thực thi khoảng thời gian tối thiểu liên tục là 1 giờ trên các lần khởi động lại ứng dụng và thiết bị. UpdateInfoService ghi lại từng lần thử trước khi gọi fetchUpdates(), vì vậy, nếu fetchUpdates() gửi một ngoại lệ (trả về UpdateFetchOutcome.FAILED sau khi gọi onFetchFailed(e)), các truy vấn tiếp theo trong 60 phút tiếp theo sẽ trả về dữ liệu dự phòng được lưu vào bộ nhớ đệm một cách suôn sẻ với kết quả UpdateFetchOutcome.THROTTLED.

Khả năng quan sát, đo từ xa và chẩn đoán

UpdateInfoService cung cấp các lệnh gọi có thể quan sát được tích hợp sẵn để theo dõi việc áp dụng ứng dụng khách, giám sát độ trễ IPC và ghi nhật ký lỗi phụ trợ mà không cần đo lường các gốc AIDL cấp thấp:

Ghi đè các lệnh gọi lại này trong UpdateInfoService (Kotlin) hoặc 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);
    }
}

Kết quả đo từ xa và chỉ số độ trễ

UpdateCheckTelemetry đo lường khoảng thời gian đã trôi qua đơn điệu (SystemClock.elapsedRealtime()) và báo cáo một trong năm kết quả được xác định trong UpdateFetchOutcome:

Hằng số kết quả @IntDef Mã Các thuộc tính chỉ số được ghi lại Nội dung mô tả và trạng thái hệ thống
UpdateFetchOutcome.CACHE_HIT 1 totalDurationMillis, processingDurationMillis, callerUid Được phân phát ngay lập tức từ bộ nhớ đệm của bộ nhớ/ổ đĩa cục bộ trên Đường dẫn nhanh (shouldFetchUpdates() trả về false). lockWaitDurationMillis và fetchDurationMillis là 0.
UpdateFetchOutcome.COALESCED 2 totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid Truy vấn được xếp hàng đợi sau một quy trình làm mới đang hoạt động khác; sau khi có được khoá, dữ liệu đã được làm mới. Tránh tìm nạp mạng trùng lặp (fetchDurationMillis là 0).
UpdateFetchOutcome.FETCHED 3 totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis, callerUid Đã thực hiện thành công quá trình đồng bộ hoá mạng phụ trợ (fetchUpdates() đã hoàn tất). Các bản ghi mới được lưu trữ trên ổ đĩa.
UpdateFetchOutcome.THROTTLED 4 totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid Yêu cầu bị chặn bởi bộ giới hạn tốc độ (shouldThrottle() trả về true). Dữ liệu đã lưu vào bộ nhớ đệm được trả về một cách an toàn cho ứng dụng (fetchDurationMillis là 0).
UpdateFetchOutcome.FAILED 5 totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis, callerUid Quá trình kiểm tra bản cập nhật hoặc yêu cầu mạng đã gặp một ngoại lệ. Bị tường lửa ngoại lệ chặn, onFetchFailed(e) đã kích hoạt, trả về dự phòng được lưu vào bộ nhớ đệm.

Lệnh gọi nâng cao của trình môi giới dịch vụ: getCallerUid()

Khi một ứng dụng kết nối, UpdateInfoService sẽ tự động đánh giá getCallerUid() trên luồng Binder ban đầu (trước khi gọi Binder.clearCallingIdentity() trước fetchUpdates()), xác minh quyền sở hữu gói và truyền trực tiếp UID của người gọi đã xác minh đến onClientConnected(), onClientDisconnected() và telemetry.callerUid trong onRequestCompleted(telemetry).

Đối với các thành phần <service> Android tiêu chuẩn, bạn không cần gọi hoặc ghi đè getCallerUid(). Phương thức protected open getCallerUid() (theo mặc định, phương thức này uỷ quyền cho Binder.getCallingUid()) được cung cấp dưới dạng một lệnh ghi đè cho các ứng dụng lưu trữ định tuyến Binder IPC thông qua một trình môi giới dịch vụ nội bộ hoặc cấu trúc proxy, cho phép lớp con trả về UID ứng dụng khách logic thay vì UID của trình môi giới.

Tài nguyên khác

Để biết thêm thông tin về việc xuất bản trạng thái bảo mật, hãy xem các tài nguyên sau:

Tài liệu

Tài liệu tham khảo API