نشر حالة أمان الجهاز

إذا كنت مصنّعًا أصليًا للأجهزة (OEM) أو كنت تحتفظ بعميل تحديث عبر الأثير (OTA) يتمتع بامتيازات، يمكنك منح التطبيقات التي تتطلّب مستوى أمان عاليًا على الجهاز إذن الوصول إلى تحديثات الأمان المعلّقة حتى تتمكّن من تقييم حالة أمان الجهاز بدقة. لفرض سياسات فعّالة تستند إلى مبدأ "عدم الثقة مطلقًا"، يجب أن تتمكّن التطبيقات من التحقّق ليس فقط من مستوى رمز تصحيح الأمان المثبَّت على الجهاز (مستوى رمز تصحيح أمان الجهاز أو DSPL)، ولكن أيضًا من تحديثات الأمان المتوفّرة والجاهزة للتثبيت (مستوى رمز تصحيح الأمان المتوفّر أو ASPL).

بما أنّ تطبيقات العميل التي لا تملك امتيازات لا يمكنها قراءة خصائص البرامج الثابتة مباشرةً أو فحص قواعد بيانات أدوات التحديث الخاصة أو طلب البحث من نقاط نهاية الخلفية الداخلية لمصنّع المعدات الأصلية، توفّر مكتبة AndroidX Security State Provider بنية موحّدة وآمنة للاتصال بين العمليات (IPC) تستخدمها أدوات تحديث العميل لمشاركة معلومات حول التحديثات المتاحة. من خلال تنفيذ UpdateInfoService في برنامج OTA، يمكنك نشر بيانات وصفية لنظام ASPL بدون الكشف عن عمليات الدمج الخاصة بالخادم الخلفي. في حين أنّ Google توفّر عمليات تنفيذ محدّثات مكوّنات النظام النمطية (Mainline) لأجهزة GMS، يمكن أيضًا للأجهزة غير التابعة لـ GMS نشر البيانات الوصفية لمستوى رمز تصحيح أمان Android (ASPL) لهذه المكوّنات النمطية للنظام.

نظرة عامة على البنية

يوضّح المخطّط التالي كيف تنشئ مكتبة AndroidX Security State Provider إطار عمل موحّدًا وآمنًا للتواصل البيني للعمليات (IPC) بين تطبيقات العميل غير المميزة وخدمات التحديث على الجهاز فقط:

تنشئ مكتبة AndroidX Security State Provider إطار عمل موحّدًا وآمنًا للاتصال بين العمليات (IPC) بين تطبيقات العميل غير المميزة وخدمات التحديث على الجهاز فقط.

نماذج تسليم البيانات

تتحقّق تطبيقات العميل من توفّر التحديثات من خلال استدعاء queryAllAvailableUpdates أو fetchAvailableSecurityPatchLevel. في الخلفية، تكتشف مكتبة البرامج تلقائيًا جميع الخدمات المسجّلة التي توسّع فئة UpdateInfoService على الجهاز من تطبيقات النظام التي لديها إذن READ_PRIVILEGED_PHONE_STATE، وتربطها بها.

كما هو موضّح في المخطّط البياني السابق، تتوافق مكتبة security-state-provider مع نموذجين لعرض البيانات:

نموذج التسليم مشغّل المزامنة استجابة العميل حالات الاستخدام المقترَحة
نموذج الإرسال (المزامنة في الخلفية) تتم مزامنة العمليات المجدوَلة التي تعمل في الخلفية (WorkManager أو JobScheduler) مع الخلفية وتكتب السجلات في UpdateInfoManager. تعرض خدمتك المحتوى دائمًا من ذاكرة التخزين المؤقت للقرص المحلي (shouldFetchUpdates() = false). يتم عرضها على الفور من ذاكرة التخزين المؤقت المحلية. أدوات تحديث النظام عبر شبكة غير سلكية (OTA) من الشركة المصنّعة للجهاز، وأدوات تحديث المكوّنات النموذجية التي تتم مزامنتها في الخلفية
سحب النموذج (المزامنة عند الطلب) تؤدي طلبات البحث الواردة من العميل بشأن عملية الاتصال بين العمليات (IPC) إلى استرجاع البيانات من الشبكة عندما تكون السجلات المخزّنة مؤقتًا قديمة (shouldFetchUpdates() = true). وتعمل ميزة دمج عمليات الإغلاق المتبادل وتقييد المعدل (shouldThrottle()) على حماية الخلفية من الارتفاعات المفاجئة. ينتظر عملية الجلب من الخلفية عندما تكون ذاكرة التخزين المؤقت قديمة. أدوات تحديث OTA أحادية البنية من مصنّعي المعدات الأصلية بدون مشغّلي مزامنة مجدولة في الخلفية

مقدّمو تحديثات متعددون

على أجهزة Android المخصّصة للإنتاج، تتوفّر عدة جهات مستقلة لتوفير التحديثات في الوقت نفسه. على سبيل المثال، تنشر Mainline معلومات التوفّر للمكوّنات النمطية (COMPONENT_SYSTEM_MODULES)، بينما ينشر برنامج OTA الخاص بمصنّع المعدات الأصلية التحديثات لصورة نظام التشغيل الأساسية (COMPONENT_SYSTEM).

يجب أن تسجّل خدمتك التحديثات للمكوّنات المحدّدة التي تديرها فقط. إذا نشر العديد من مقدّمي الخدمات على أحد الأجهزة تحديثات للمكوّن نفسه، ستقيِّم تطبيقات العميل أعلى مستوى تصحيح متاح (باستخدام fetchAvailableSecurityPatchLevel()) أو ستفحص سجلات UpdateInfo الفردية (باستخدام queryAllAvailableUpdates()) لأغراض التدقيق في المؤسسات. تأكَّد من أنّ خدمتك تنشر دائمًا التنسيق الأساسي للمكوّن (DateBasedSecurityPatchLevel لـ COMPONENT_SYSTEM).

دليل مفصّل لإعداد عميل التحديث

اتّبِع الخطوات التالية لدمج مكتبة AndroidX Security State Provider في برنامج التحديث والبدء في نشر معلومات حول توفّر تحديث الأمان لجهازك.

الخطوة 1: إضافة التبعيات

لتنفيذ موفّر تحديث، تأكَّد من أنّ مشروعك يتضمّن مستودع Google Maven، ثم أضِف مكتبة security-state-provider إلى ملف build.gradle.kts (Kotlin DSL) أو build.gradle (Groovy DSL) في الوحدة:

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

الخطوة 2: تعريف خدمة التحديث في ملف البيان

يجب تعريف خدمتك في 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".

الخطوة 3: تنفيذ UpdateInfoService

لنشر حالة التحديث، يجب تنفيذ فئة UpdateInfoService وإنشاء سجلّات تحديث تطابق نموذج البيانات المتوقّع.

مواصفات نموذج بيانات UpdateInfo

سواء اخترت نموذج الدفع أو نموذج السحب، أنشئ سجلات UpdateInfo باستخدام UpdateInfo.Builder وفقًا للمواصفات التالية:

اسم الحقل Getter Method نوع البيانات متطلبات التحقّق من الصحة والتنسيق الغرض والترميز الدلالي للنظام
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 عدد الملّي ثانية منذ بدء حقبة Unix (System.currentTimeMillis())، ويجب أن يكون > 0. عندما أصبح التحديث متاحًا للمستخدمين، مثل وقت الإصدار عبر الهواء لا تستخدِم وقت تنزيل الحمولة أو تثبيتها.
lastCheckTimeMillis getLastCheckTimeMillis() long عدد الملّي ثانية منذ بدء حقبة يونكس يجب أن تكون > 0. الطابع الزمني الذي تحقّق فيه مقدّم الخدمة من سجلّ التعديل هذا أو رصده أثناء المزامنة

اختَر نموذج التسليم الذي يناسب بنية أداة التحديث من الخيارات التالية:

الخيار (أ): نموذج الدفع (مقترَح)

عندما يتحقّق عامل المزامنة في الخلفية من خادم OTA، تأكَّد من أنّ أي تحديث تم اكتشافه يؤدي إلى تقدّم مستوى تصحيح الجهاز الحالي واحتفظ به باستخدام UpdateInfoManager.registerUpdate()، أو اتّصِل بـ UpdateInfoManager.unregisterUpdate() إذا لم يكن هناك أي تحديث أمان متقدّم معلّق. يجب دائمًا استدعاء UpdateInfoManager.setLastCheckTimeMillis() في نهاية كل عملية مزامنة (حتى بعد استدعاء registerUpdate()، الذي يحفظ سجل UpdateInfo لكل مكون ولكن لا يعدّل الطابع الزمني العام لآخر عملية تحقّق الذي يتم عرضه للعملاء). يمكنك تنفيذ ذلك باستخدام WorkManager CoroutineWorker في Kotlin أو Worker في 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();
    }
}

في النموذج المستند إلى الإشعارات، تحتفظ المهام التي تعمل في الخلفية بسجلات التعديل مباشرةً في UpdateInfoManager. لتوجيه إطار العمل إلى عرض السجلات دائمًا من مساحة التخزين على القرص المحلي، عليك إلغاء shouldFetchUpdates() لعرض false من خلال توسيع UpdateInfoService في Kotlin أو ListenableFutureUpdateInfoService في 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());
    }
}

الخيار (ب): نموذج السحب (عند الطلب)

في بنية مستندة إلى السحب، تعالج خدمتك طلبات إعادة التحميل عند الطلب التي يتم تشغيلها بواسطة تطبيقات العميل عندما تكون ذاكرة التخزين المؤقت المحلية قديمة.

للتعامل مع طلبات التحديث عند الطلب، يمكنك توسيع نطاق UpdateInfoService في Kotlin (من خلال تنفيذ الدالة المعلقة fetchUpdates()) أو ListenableFutureUpdateInfoService في Java (من خلال تنفيذ fetchUpdatesAsync() التي تعرض 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);
    }
}

الخطوة 4: محو التحديثات التي تم تطبيقها بعد إعادة تشغيل الجهاز

على الرغم من أنّ UpdateInfoManager يزيل تلقائيًا التحديثات القديمة عند استدعاء registerUpdate()، لن يستدعي برنامج التحديث registerUpdate() مرة أخرى بعد انتهاء تثبيت تحديث عبر الأثير (OTA) إلى أن يحين موعد دورة المزامنة التالية المجدوَلة مع الخادم. لمنع تطبيقات العميل من اعتبار التحديث المثبَّت مسبقًا معلّقًا بعد إعادة التشغيل مباشرةً، استمع إلى ACTION_BOOT_COMPLETED واستدعِ UpdateInfoManager.unregisterUpdate() عند انتهاء تثبيت تحديث عبر الأثير (OTA) لمحو السجلّ من ذاكرة التخزين المؤقت المحلية. ويتم إجراء ذلك فقط عند الانتهاء من تثبيت أحد التحديثات، ما يمنع محو التحديثات المعلقة (التي لم يتم تثبيتها) بشكل غير مشروط عند كل عملية إعادة تشغيل عادية للجهاز. بما أنّ مفاتيح UpdateInfoManager تعدّل السجلات حسب المكوّن، ما عليك سوى تحديد المكوّن المستهدف عند إنشاء عنصر UpdateInfo لإلغاء التسجيل:

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

الخطوة 5: التحقّق من عملية التكامل

أجرِ عمليات التحقّق التالية على جهاز Android أو محاكي باستخدام Android Debug Bridge (ADB) للتحقّق من صحة عملية الدمج التام بين الأطراف وتجنُّب المشاكل الشائعة التي قد تحدث عند نشر الشركات المصنّعة الأصلية (OEM):

  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. التحقّق من حلّ الغرض في كل من "المستخدم 0" و"ملف العمل": تأكَّد من أنّ نظام التشغيل Android PackageManager يحلّ فلتر الأغراض UPDATE_INFO_SERVICE الذي تم تصديره في كل من المستخدم الأساسي (User 0) وأي ملف عمل نشط في Android Enterprise (مثل 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. التحقّق من حالة الخدمة والسجلات المخزّنة مؤقتًا باستخدام 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))، ستعرض الاستعلامات اللاحقة خلال فترة السماح البالغة 60 دقيقة بيانات احتياطية مخزّنة مؤقتًا مع النتيجة UpdateFetchOutcome.THROTTLED.

إمكانية تتبُّع البيانات والقياس عن بُعد والتشخيص

توفّر UpdateInfoService نقاط ربط مدمجة لإمكانية تتبُّع البيانات بهدف تتبُّع معدّل استخدام العملاء، ومراقبة وقت استجابة الاتصال بين العمليات (IPC)، وتسجيل أخطاء الخلفية بدون استخدام أدوات لإنشاء رموز AIDL الأساسية:

يمكنك إلغاء هاتين الدالتين في UpdateInfoService (Kotlin) أو 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);
    }
}

نتائج القياس عن بُعد ومقاييس وقت الاستجابة

يقيس 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)، وتم عرض المحتوى الاحتياطي المخزّن مؤقتًا.

Advanced service broker hook: getCallerUid()

عندما يتصل أحد العملاء، تقيِّم UpdateInfoService تلقائيًا getCallerUid() في سلسلة Binder الأولية (قبل استدعاء Binder.clearCallingIdentity() قبل fetchUpdates())، وتتحقّق من ملكية الحزمة، وتمرّر معرّف المستخدم (UID) الخاص بالمتصل الذي تم التحقّق منه مباشرةً إلى onClientConnected() وonClientDisconnected() وtelemetry.callerUid في onRequestCompleted(telemetry).

بالنسبة إلى مكوّنات Android العادية <service>، ليس عليك استدعاء أو إلغاء getCallerUid(). يتم توفير طريقة protected open getCallerUid() (التي يتم تفويضها إلى Binder.getCallingUid() تلقائيًا) كخطاف إلغاء للتطبيقات المضيفة التي توجّه Binder IPC من خلال وسيط خدمة داخلي أو بنية وكيل، ما يتيح للفئة الفرعية عرض معرّف UID المنطقي للعميل بدلاً من معرّف UID الخاص بالوسيط.

مراجع إضافية

لمزيد من المعلومات حول حالة أمان النشر، يُرجى الاطّلاع على المراجع التالية:

الوثائق

دليل API المرجعي