وضعیت امنیت دستگاه را منتشر کنید

اگر سازنده تجهیزات اصلی (OEM) هستید یا از مشتری به‌روزرسانی ممتاز ازطریق هوا (OTA) نگهداری می‌کنید، می‌توانید به برنامه‌های حساس به امنیت در دستگاه امکان مشاهده به‌روزرسانی‌های امنیتی معلقه را بدهید تا بتوانند وضعیت امنیتی دستگاه را به‌دقت ارزیابی کنند. برای اعمال خط‌مشی‌های قدرتمند اعتماد صفر، برنامه‌ها باید بتوانند نه تنها سطح وصله نصب‌شده روی دستگاه (سطح وصله امنیتی دستگاه، یا DSPL) را تأیید کنند، بلکه به‌روزرسانی‌های امنیتی دردسترس و آماده نصب (سطح وصله امنیتی دردسترس، یا ASPL) را نیز تأیید کنند.

ازآنجایی‌که برنامه‌های مشتری بدون امتیاز نمی‌توانند مستقیماً ویژگی‌های سخت‌افزار را بخوانند، پایگاه‌های داده به‌روزرسان خصوصی را بازرسی کنند، یا از نقاط پایانی داخلی OEM backend پُرسمان کنند، کتابخانه AndroidX Security State Provider معماری استاندارد و امنی برای ارتباط بین‌پردازشی (IPC) ارائه می‌دهد که مشتریان به‌روزرسانی از آن برای هم‌رسانی اطلاعات درباره به‌روزرسانی‌های دردسترس استفاده می‌کنند. با پیاده‌سازی UpdateInfoService در مشتری به‌روزرسانی OTA، می‌توانید فراداده ASPL را برای سیستم بدون آشکار کردن یکپارچه‌سازی‌های زیرینه اختصاصی منتشر کنید. درحالی‌که Google پیاده‌سازی عنصر سیستم واحد (Mainline) به‌روزرسان‌ها را برای دستگاه‌های GMS ارائه می‌دهد، دستگاه‌های غیر GMS نیز می‌توانند فراداده ASPL را برای این عناصر سیستم واحد منتشر کنند.

نمای کلی معماری

نمودار زیر نشان می‌دهد کتابخانه AndroidX Security State Provider چگونه چارچوب IPC استاندارد و امنی بین برنامه‌های مشتری بدون امتیاز و خدمات به‌روزرسانی درون‌دستگاهی ایجاد می‌کند:

کتابخانه «ارائه‌دهنده وضعیت امنیت AndroidX» چارچوب IPC استاندارد و امنی بین برنامه‌های کارخواه بدون امتیاز و خدمات به‌روزرسانی درون‌دستگاهی ایجاد می‌کند

مدل‌های ارسال داده

برنامه‌های کارخواه با فراخواندن queryAllAvailableUpdates یا fetchAvailableSecurityPatchLevel دردسترس بودن به‌روزرسانی را پُرسمان می‌کنند. در پشت صحنه، کتابخانه کارخواه به‌طور خودکار همه سرویس‌های ثبت‌شده‌ای را که کلاس UpdateInfoService را در دستگاه از برنامه‌های سیستمی که اجازه READ_PRIVILEGED_PHONE_STATE را دارند گسترش می‌دهند پیدا می‌کند و به آن‌ها ملحق می‌شود.

همان‌طور که در نمودار قبلی نشان داده شده است، کتابخانه security-state-provider از دو مدل ارائه داده پشتیبانی می‌کند:

مدل ارائه راه‌انداز همگام‌سازی پاسخ مشتری موارد استفاده توصیه‌شده
مدل فشاری (همگام‌سازی پس‌زمینه) کارگران پس‌زمینه زمان‌بندی‌شده (WorkManager یا JobScheduler) با زیرینه شما همگام‌سازی می‌کنند و گزارش‌ها را در UpdateInfoManager می‌نویسند. خدمات شما همیشه از حافظه نهان دیسک محلی (shouldFetchUpdates() = false) ارائه می‌شود. بلافاصله از حافظه نهان محلی ارائه شد. به‌روزرسان‌های OTA سیستم OEM و به‌روزرسان‌های مؤلفه واحد همگام‌سازی‌شده در پس‌زمینه.
کشیدن مدل (همگام‌سازی درصورت نیاز) وقتی گزارش‌های ذخیره‌شده در حافظه نهان قدیمی باشند (shouldFetchUpdates() = true)، پُرسمان‌های IPC مشتری ورودی باعث واکشی شبکه می‌شوند. ادغام متقابل و محدود کردن نرخ (shouldThrottle()) از زیرینه شما دربرابر جهش‌های ناگهانی محافظت می‌کند. وقتی حافظه نهان قدیمی است، منتظر واکشی زیرینه می‌ماند. به‌روزرسان‌های OTA سازنده اصلی محصول یکپارچه بدون کارگران همگام‌سازی پس‌زمینه زمان‌بندی‌شده.

چندین ارائه‌دهنده به‌روزرسانی

در دستگاه‌های Android تولیدی، چندین ارائه‌دهنده به‌روزرسانی مستقل به‌طور هم‌زمان با هم همزیستی دارند. برای مثال، Mainline دردسترس بودن مؤلفه‌های واحدی (COMPONENT_SYSTEM_MODULES) را منتشر می‌کند، درحالی‌که کارخواه OTA سازنده تجهیزات اصلی به‌روزرسانی‌های تصویر سیستم عامل اصلی (COMPONENT_SYSTEM) را منتشر می‌کند.

خدمات شما فقط باید به‌روزرسانی‌های مربوط به اجزای خاصی را که مدیریت می‌کند ثبت کند. اگر چندین ارائه‌دهنده در یک دستگاه به‌روزرسانی‌هایی را برای یک جزء منتشر کنند، برنامه‌های کارخواه بالاترین سطح وصله دردسترس را (بااستفاده از fetchAvailableSecurityPatchLevel()) ارزیابی می‌کنند یا گزارش‌های UpdateInfo فردی را (بااستفاده از queryAllAvailableUpdates()) برای حسابرسی سازمانی بازرسی می‌کنند. مطمئن شوید که سرویس شما همیشه قالب متعارف را برای عنصرتان منتشر می‌کند (DateBasedSecurityPatchLevel برای COMPONENT_SYSTEM).

راهنمای گام‌به‌گام برای آماده‌سازی مشتری به‌روزرسانی

برای ادغام کتابخانه «ارائه‌دهنده وضعیت امنیت AndroidX» در کارخواه به‌روزرسانی و شروع انتشار دردسترس بودن به‌روزرسانی امنیتی دستگاهتان، این مراحل را دنبال کنید.

مرحله ۱: افزودن وابستگی‌ها

برای پیاده‌سازی ارائه‌دهنده به‌روزرسانی، مطمئن شوید پروژه شما شامل مخزن Google Maven باشد، سپس کتابخانه security-state-provider را به فایل build.gradle.kts (Kotlin DSL) یا build.gradle (Groovy DSL) واحد خود اضافه کنید:

کاتلین

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

مرحله ۲: سرویس به‌روزرسانی را در مانیفست خود اعلام کنید

سرویس خود را در AndroidManifest.xml برنامه با <intent-filter> مطابقت با androidx.security.state.provider.UPDATE_INFO_SERVICE اعلام کنید. سرویس باید صادر شود (android:exported="true") و به‌عنوان سرویس تک‌کاربر پیکربندی شود (android:singleUser="true") تا کتابخانه کارخواه بتواند در سراسر مرزهای کاربر و فرایند به آن متصل شود، به‌ویژه برای نمایه‌های کاری:

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

اگر به‌روزرسان شما به‌عنوان android.uid.system اجرا نمی‌شود، اجازه‌های زیر را نیز در مانیفست خود اعلام کنید و مطمئن شوید آن‌ها را به فهرست مجاز اجازه‌های ممتاز خود اضافه کرده‌اید:

  • ‫READ_PRIVILEGED_PHONE_STATE: برای اینکه مشتریان به ارائه‌دهنده شما اعتماد کنند لازم است.
  • ‫INTERACT_ACROSS_USERS: برای android:singleUser="true" لازم است.

مرحله ۳: پیاده‌سازی UpdateInfoService

برای انتشار وضعیت به‌روزرسانی، باید کلاس UpdateInfoService را پیاده‌سازی کنید و گزارش‌های به‌روزرسانی را بسازید که با مدل داده موردانتظار مطابقت داشته باشد.

مشخصات مدل داده UpdateInfo

چه مدل «فشار» را انتخاب کنید چه مدل «کشیدن»، UpdateInfo گزارش‌ها را بااستفاده از UpdateInfo.Builder طبق مشخصات زیر بسازید:

نام فیلد روش دریافت‌کننده نوع داده الزامات قالب و اعتبارسنجی هدف و معناشناسی سیستم
component getComponent() ‫String‏ (@Component) ثابت‌های کانونی در SecurityPatchState: COMPONENT_SYSTEM، COMPONENT_SYSTEM_MODULES، یا COMPONENT_KERNEL. زیرسیستم نرم‌افزار یا سفت‌افزاری را که این به‌روزرسانی هدف قرار می‌دهد شناسایی می‌کند.
securityPatchLevel getSecurityPatchLevel() SecurityPatchLevel باید نمونه‌ای از DateBasedSecurityPatchLevel (YYYY-MM-DD) یا VersionedSecurityPatchLevel (major.minor.patch) باشد، یا بااستفاده از SecurityPatchState.getComponentSecurityPatchLevel() تجزیه شود. سطح وصله امنیتی هدف که پس‌از نصب این به‌روزرسانی به آن دست خواهید یافت.
publishedDateMillis getPublishedDateMillis() long میلی‌ثانیه از تاریخ یونیکس (System.currentTimeMillis()). باید > 0 باشد. زمانی که به‌روزرسانی برای کاربران دردسترس قرار گرفت، مثلاً زمان انتشار OTA. از زمان بارگیری یا نصب پایه‌بار استفاده نکنید.
lastCheckTimeMillis getLastCheckTimeMillis() long میلی‌ثانیه از تاریخ یونیکس. باید > 0 باشد. مُهر زمان زمانی که ارائه‌دهنده شما این گزارش به‌روزرسانی را درحین همگام‌سازی درستی‌سنجی یا کشف کرده است.

از بین گزینه‌های زیر، مدل ارائه را که با معماری به‌روزرسان شما مطابقت دارد انتخاب کنید:

گزینه الف: مدل فشار (توصیه‌شده)

وقتی کارگر همگام‌سازی پس‌زمینه‌ای سرور OTA شما را بررسی می‌کند، تأیید کنید که هر به‌روزرسانی کشف‌شده‌ای سطح وصله فعلی دستگاه را ارتقا می‌دهد و آن را بااستفاده از UpdateInfoManager.registerUpdate() حفظ می‌کند، یا اگر به‌روزرسانی امنیتی ارتقادهنده‌ای درانتظار نیست، UpdateInfoManager.unregisterUpdate() را فراخوانی می‌کند. همیشه در پایان هر همگام‌سازی (حتی پس‌از فراخوانی registerUpdate() که سابقه UpdateInfo هر عنصر را حفظ می‌کند اما مُهر زمان آخرین بررسی سراسری را که به مشتریان برگردانده می‌شود به‌روز نمی‌کند) UpdateInfoManager.setLastCheckTimeMillis() را فراخوانی کنید. می‌توانید این را با WorkManager CoroutineWorker در Kotlin یا Worker در Java پیاده‌سازی کنید:

کاتلین

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

جاوا

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

در مدل مبتنی بر فشار، تکالیف پس‌زمینه سوابق به‌روزرسانی را مستقیماً در UpdateInfoManager ذخیره می‌کنند. برای اینکه چارچوب را راهنمایی کنید همیشه گزارش‌ها را از فضای ذخیره‌سازی دیسک محلی ارائه دهد، با گسترش UpdateInfoService در Kotlin یا ListenableFutureUpdateInfoService در Java، shouldFetchUpdates() را ملغی کنید تا false را برگرداند:

کاتلین

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

جاوا

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

گزینه ب: مدل کششی (درصورت نیاز)

در معماری مبتنی بر واکشی، سرویس شما درخواست‌های بازآوری درخواستی را که برنامه‌های کارخواه وقتی حافظه نهان محلی قدیمی می‌شود راه‌اندازی می‌کنند مدیریت می‌کند.

برای مدیریت پُرسمان‌های به‌روزرسانی درخواستی، UpdateInfoService را در Kotlin گسترش دهید (با پیاده‌سازی تابع تعلیق‌کننده fetchUpdates()) یا ListenableFutureUpdateInfoService را در جاوا (با پیاده‌سازی fetchUpdatesAsync() که ListenableFuture ‏Guava را برمی‌گرداند):

کاتلین

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

جاوا

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

مرحله ۴: پاک کردن به‌روزرسانی‌های اعمال‌شده پس‌از بازراه‌اندازی دستگاه

اگرچه UpdateInfoManager به‌طور خودکار به‌روزرسانی‌های منسوخ را هرزمان registerUpdate() فراخوانی شود حذف می‌کند، به‌روزرسان شما پس‌از اتمام نصب به‌روزرسانی ازطریق OTA تا چرخه همگام‌سازی سرور زمان‌بندی‌شده بعدی، registerUpdate() را دوباره فراخوانی نخواهد کرد. برای جلوگیری از اینکه برنامه‌های مشتری به‌روزرسانی ازقبل نصب‌شده را بلافاصله پس‌از بازراه‌اندازی همچنان معلقه ببینند، به ACTION_BOOT_COMPLETED گوش دهید و وقتی نصب به‌روزرسانی OTA تمام شد، UpdateInfoManager.unregisterUpdate() را فراخوانی کنید تا سابقه از حافظه نهان محلی پاک شود. انجام این کار فقط زمانی که نصب به‌روزرسانی تمام شده باشد، از پاک کردن بی‌قیدوشرط به‌روزرسانی‌های معلقه (حذف نصب‌شده) در هر بازراه‌اندازی عادی دستگاه جلوگیری می‌کند. چون کلیدهای UpdateInfoManager گزارش‌ها را براساس عنصر به‌روزرسانی می‌کنند، هنگام ساختن شیء UpdateInfo برای لغو ثبت، فقط باید عنصر هدف را مشخص کنید:

کاتلین

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

جاوا

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

مرحله ۵: یکپارچه‌سازی را به‌تأیید برسانید

برای اعتبارسنجی یکپارچه‌سازی سرتاسر و جلوگیری از مشکلات رایج در استقرار «تولیدکننده تجهیزات اصلی» (OEM)، بررسی‌های زیر را در دستگاه Android یا شبیه‌ساز بااستفاده از Android Debug Bridge (ADB) اجرا کنید:

  1. تأیید کنید که مشتریان به ارائه‌دهنده شما اعتماد دارند: برنامه‌های مشتری هر ارائه‌دهنده‌ای را که READ_PRIVILEGED_PHONE_STATE نداشته باشد نادیده می‌گیرند، حتی اگر ازقبل نصب شده باشد. تأیید کنید که اجازه اعطا شده است:

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

    سپس تأیید کنید که سرویس شما قابل‌کشف است و اجازه سرویس ندارد. در برونداد سرویس خود، exported=true و permission=null را بررسی کنید:

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

    اگر مشتری همچنان ارائه‌دهنده شما را نمی‌بیند، logcat را برای Ignoring untrusted update provider از برچسب SecurityPatchState بررسی کنید.

  2. درستی‌سنجی هدف در «کاربر ۰» و «نمایه‌های کاری»: تأیید کنید که سیستم‌عامل Android فیلتر هدف صادرشده PackageManager شما را در کاربر اصلی (User 0) و هر «نمایه کاری Android Enterprise» فعال (مثل User 10) حل‌وفصل می‌کند: UPDATE_INFO_SERVICE

    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. وضعیت خدمات و سوابق ذخیره‌شده در حافظه نهان را بااستفاده از dumpsys درستی‌سنجی کنید: UpdateInfoService dump() را برای گزارش کردن Global Last Check، Should Throttle (وضعیت محدودکننده نرخ)، و Cached Updates ملغی می‌کند. چون UpdateInfoService سرویس محدود است و کارخواه‌ها بلافاصله پس‌از پُرسمان کردن لغو محدودیت می‌کنند، dumpsys activity service وقتی هیچ کارخواهی محدود نباشد (nothing) را برونداد می‌کند. پیش‌از اجرای 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>
    

    برونداد تشخیص خرابی نمونه:

    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. راه‌اندازی اتصال کارخواه و تأیید نتایج تله‌متری: از یک برنامه آزمایشی بدون امتیاز (بدون داشتن اجازه‌های امضای سیستم)، SecurityPatchState.queryAllAvailableUpdates() را فراخوانی کنید. اگر بازخوان‌های تله‌متری را پیاده‌سازی کرده‌اید، موارد زیر را بررسی کنید:

    • تأیید کنید که مشتری بدون امتیاز ویژه بدون SecurityException پیوند می‌دهد و onClientConnected(packageName, callerUid) را راه‌اندازی می‌کند.
    • برای ارائه‌دهندگان مدل «فشاری» (shouldFetchUpdates() == false): تأیید کنید که onRequestCompleted(telemetry) گزارش‌های UpdateFetchOutcome.CACHE_HIT (1) را با fetchDurationMillis == 0 در هر پُرسمان ثبت می‌کند.
    • برای ارائه‌دهندگان مدل کششی (shouldFetchUpdates() == true): تأیید کنید که onRequestCompleted(telemetry) گزارش‌های UpdateFetchOutcome.FETCHED (3) در پُرسمان اولیه حافظه نهان قدیمی، و سپس CACHE_HIT (1) در پُرسمان‌های بعدی فوری وجود دارد. (برای بازنشانی محدودکننده نرخ ماندگار ۱ ساعته بین اجراهای آزمایشی مدل کششی، adb shell pm clear <your_package_name> را اجرا کنید.)

پیکربندی‌های اختیاری و پیشرفته

خط‌مشی ذخیره در حافظه نهان و محدود کردن نرخ

وقتی کارخواهی به‌روزرسانی‌ها را پُرسمان می‌کند، UpdateInfoService گردش کار قفل‌گذاری با بررسی مضاعف را اجرا می‌کند تا تازگی داده‌ها را دربرابر بار سرور زیرینه متعادل کند:

‫UpdateInfoService گردش کار قفل‌گذاری با بررسی مضاعف را اجرا می‌کند تا بین جدید بودن داده‌ها و بار سرور زیرینه تعادل برقرار کند

  • مسیر سریع (shouldFetchUpdates()): به‌طور پیش‌فرض، shouldFetchUpdates() فقط زمانی true (نشان‌دهنده حافظه نهان قدیمی) برمی‌گرداند که lastCheckTimeMillis جهانی قدیمی‌تر از ۱ ساعت باشد (TimeUnit.HOURS.toMillis(1)). وقتی shouldFetchUpdates()‏ false برمی‌گرداند، سرویس بلافاصله گزارش‌های ذخیره‌شده در حافظه نهان را با پیامد UpdateFetchOutcome.CACHE_HIT بدون دریافت قفل یا انجام ورودی/خروجی شبکه برمی‌گرداند. برای سفارشی‌سازی این خط‌مشی ذخیره در حافظه نهان، می‌توانید shouldFetchUpdates() را ملغی کنید.
  • مسیر کند و ادغام درخواست: وقتی shouldFetchUpdates() برمی‌گردد true، سرویس یک متقابل انحصاری روتین داخلی به‌دست می‌آورد و shouldFetchUpdates() را دوباره ارزیابی می‌کند (اگر درخواست هم‌زمان درحالی‌که منتظر قفل است حافظه نهان را ازقبل بازآوری کرده باشد، UpdateFetchOutcome.COALESCED را برمی‌گرداند).
  • محدودکننده نرخ ماندگار (shouldThrottle()): برای محافظت از زیرساخت پشتیبان دربرابر هجوم پُرسمان یا خطاهای مکرر، shouldThrottle() حداقل فاصله ۱ ساعته ماندگاری را در بازراه‌اندازی‌های برنامه و دستگاه اعمال می‌کند. ‫UpdateInfoService هر تلاش را قبل‌از فراخوانی fetchUpdates() ثبت می‌کند، بنابراین اگر fetchUpdates() استثنایی ایجاد کند (UpdateFetchOutcome.FAILED را پس‌از فراخوانی onFetchFailed(e) برمی‌گرداند)، پُرسمان‌های بعدی درطول ۶۰ دقیقه بعدی به‌طور مناسب داده‌های جایگزین ذخیره‌شده در حافظه نهان را با نتیجه UpdateFetchOutcome.THROTTLED برمی‌گرداند.

مشاهده‌پذیری، تله‌متری، و تشخیص خرابی

‫UpdateInfoService قلاب‌های مشاهده‌پذیری داخلی برای پیگیری پذیرش مشتری، پایش تأخیر IPC، و ثبت خطاهای زیرینه بدون ابزاربندی ته‌مانده‌های AIDL سطح پایین ارائه می‌دهد:

  • onRequestCompleted(telemetry): پس‌از تکمیل هر بررسی به‌روزرسانی با خلاصه UpdateCheckTelemetry فراخوانده می‌شود.
  • onClientConnected(packageName, callerUid): زمانی فراخوانده می‌شود که یک کارخواه تأییدشده جلسه را باز می‌کند.
  • onClientDisconnected(packageName, callerUid): زمانی فراخوانده می‌شود که کارخواه لغو اتصال کند یا فرایند آن خاتمه یابد.
  • onFetchFailed(e): اگر درطول fetchUpdates()، قبل‌از اینکه سرویس داده‌های جایگزین ذخیره‌شده در حافظه نهان را برگرداند، استثنایی رخ دهد، فراخوانده می‌شود.

این برگشت‌تماس‌ها را در UpdateInfoService (Kotlin) یا ListenableFutureUpdateInfoService (Java) ملغی کنید:

کاتلین

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

جاوا

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

نتایج دورسنجی و سنجه‌های تأخیر

UpdateCheckTelemetry مدت‌های سپری‌شده یکنواخت را اندازه‌گیری می‌کند (SystemClock.elapsedRealtime()) و یکی از پنج پیامد تعریف‌شده در UpdateFetchOutcome را گزارش می‌کند:

ثابت پیامد @IntDef کد گزارش ویژگی‌های سنجه شرح و وضعیت سیستم
UpdateFetchOutcome.CACHE_HIT 1 totalDurationMillis، processingDurationMillis، callerUid بلافاصله از حافظه نهان/دیسک محلی در «مسیر سریع» ارائه شد (shouldFetchUpdates() برگشت false). lockWaitDurationMillis و fetchDurationMillis 0 هستند.
UpdateFetchOutcome.COALESCED 2 totalDurationMillis، lockWaitDurationMillis، processingDurationMillis، callerUid پُرسمان در صف بازآوری فعال دیگری قرار گرفت؛ پس‌از دریافت قفل، داده‌ها تازه بود. از واکشی شبکه تکراری (fetchDurationMillis‏ 0 است) اجتناب شد.
UpdateFetchOutcome.FETCHED 3 ‫totalDurationMillis‏، lockWaitDurationMillis‏، processingDurationMillis‏، fetchDurationMillis‏، callerUid همگام‌سازی شبکه زیرینه باموفقیت اجرا شد (fetchUpdates() تکمیل شد). گزارش‌های جدید در دیسک ماندگار شد.
UpdateFetchOutcome.THROTTLED 4 totalDurationMillis، lockWaitDurationMillis، processingDurationMillis، callerUid درخواست توسط محدودکننده نرخ مسدود شد (shouldThrottle() برگردانده شد true). داده‌های ذخیره‌شده در حافظه نهان به‌طور ایمن به کارخواه برگردانده شد (fetchDurationMillis 0 است).
UpdateFetchOutcome.FAILED 5 ‫totalDurationMillis‏، lockWaitDurationMillis‏، processingDurationMillis‏، fetchDurationMillis‏، callerUid بررسی به‌روزرسانی یا درخواست شبکه استثنایی ایجاد کرد. دیوار آتش استثنا آن را گرفته است، onFetchFailed(e) را راه‌اندازی کرده است، و برگشت‌پذیر حافظه نهان را برگردانده است.

قلاب کارگزار سرویس پیشرفته: getCallerUid()‎

وقتی مشتری متصل می‌شود، UpdateInfoService به‌طور خودکار getCallerUid() را در رشته اولیه «پوشه» (قبل‌از تماس با Binder.clearCallingIdentity() قبل‌از fetchUpdates()) ارزیابی می‌کند، مالکیت بسته را درستی‌سنجی می‌کند، و «شناسه کاربر» درستی‌سنجی‌شده تماس‌گیرنده را مستقیماً به onClientConnected()،‏ onClientDisconnected()، و telemetry.callerUid در onRequestCompleted(telemetry) ارسال می‌کند.

برای عناصر استاندارد Android <service>، نیازی به فراخوانی یا ملغی کردن getCallerUid() ندارید. روش protected open getCallerUid() (که به‌طور پیش‌فرض به Binder.getCallingUid() واگذار می‌کند) به‌عنوان قلاب ملغی برای برنامه‌های میزبان که Binder IPC را ازطریق کارگزار سرویس داخلی یا معماری پراکسی مسیریابی می‌کنند ارائه می‌شود و به زیرکلاس امکان می‌دهد به‌جای UID کارگزار، UID منطقی کارخواه را برگرداند.

منابع بیشتر

برای کسب اطلاعات بیشتر درباره انتشار وضعیت امنیت، منابع زیر را ببینید:

اسناد

مرجع میانای برنامه‌سازی کاربردی